Solid 원칙

Solid 원칙 개념 설명

Posted by Damin on September 13, 2019

Solid 원칙

SRP(단일 책임 원칙), OCP(개방-폐쇄 원칙), LS(리스코프 치환 원칙), DIP(의존 역전 원칙), ISP(인터페이스 분리 원칙)

시간이 지나도 유지 보수와 확장이 쉬운 소프트웨어 개발

단일 책임 원칙(SRP)

“객체는 단 하나의 책임만 가져야 한다”

책임

  • 해야 하는 것

  • 할 수 있는 것

  • 해야 하는 것을 잘 할 수 있는 것

예를 들어 보자.

Student(학생) 클래스에서 수강 과목을 추가 및 조회, DB에서 객체 정보 저장 및 객체 정보 읽기 등등…이 있다고 가정하자.

이렇게 되면 Student 클래스는 엄청나게 많은 책임을 수행해야 한다.

Sutent 클래스에서 가장 잘할 수 있는 것은 ‘수강 과목 추가 및 조회’ 이다.

‘DB에 있는 객체 정보 읽기 및 객체 정보 저장’ 은 Student 클래스가 아닌 다른 클래스가 더 잘할 수 있는 여지가 많다.

따라서 Student 클래스에는 ‘수강 과목 추가 및 조회’만 수행하도록 하는 것이 SRP를 따르는 설계이다!!

어찌보면 지금까지 우리는 이렇게 해왔을 수도?

변경

“설계 원칙을 학습하는 이유는 예측하지 못한 변경사항이 발생해도 유연하고 확장성잉 있도록 시스템 구조를 설계하기 위해서다”

-> 좋은 설계 = 새로운 요구사항 or 변경 사항이 있을 때 영향 받는 부분을 줄이는 것.

책임을 많이 질수록 클래스 내부에서 서로 다른 역할을 수행하는 코드끼리 강하게 결합될 가능성이 높아진다.

-> ‘수강 과목 추가’ 와 ‘객체 정보 저장’ 은 어딘가에서 연결될 수도 있다.

따라서, 이 상황에서는 ‘수강 과목 추가’ 기능을 변경하더라도

‘객체 정보 저장’ 기능을 직접 or 간접적으로 사용하는 모든 코드도 테스트 해봐야 한다.

** 회기 테스트 ** = 어떤 변화가 있을 때 해당 변화가 기존 시스템의 기능에 영향을 주는지 평가하는 테스트

책임 분리

한 클래스 안에서 너무 많은 책임을 지지 말자!!!

Student 클래스에 너무 많은 책임이 수행된다.

따라서 Student 클래스 단 하나의 책임만 수행하도록 해자!! -> 수정되더라도 테스트할게 적어진다.

산탄총 수술

산탄총의 총알에는 여러 개의 산탄이 들어 있어서, 총을 쏘면 여러 갈래로 퍼진다.

수술을 하기 위해서는 총이 맞은 모든 곳을 수술해야 한다.

수술 부분이 많아 진다!!

이 부분에 착안해 용어가 만들어 졌다고 한다.

어느 하나의 책임이 여러 개의 클래스로 분리되어 있으면

모든 클래스 하나하나를 모두 변경해야 한다. (그러지 않으면 정상적으로 동작 x)

횡단관심</br>

횡단 관심 = 많은 모듈에서 반복적으로 나타나는 것

횡단 관심중에 하나인 보안에 대해 말해보겠습니다.

‘보안’은 핵심관심(계좌이체, 입출금, 이자계산)에 필요한 책임입니다.

이런 부가 기능을 별개의 클래스로 분리해서 책임을 담당하게 한다.

-> 여러 곳에 흩어진 공통 책임을 한 곳에 모으고 응집도를 높이자!!

이해가 잘 안될 수 있는데, 더 알기 쉽게 말씀드리겠습니다!

DB 프로그램을 해 보신 분들은 공감하실 수 있을텐데

query를 넣을 때 insert, update, delete, select중 무엇을 하던

try catch를 사용하시는 분들이 있을겁니다.

이렇게 항상 같은 패턴을 횡단 관심, 다른 기능(insert, update, delete, select)은 핵심 관심이라고 보시면 됩니다!

근데 이런 독립 클래스(횡단 관심을 묶어 놓은)를 구현하더라도

구현된 기능들을 호출하고 사용하는 코드는 해당 기능을 사용하는 코드 어딘가에 포함될 수 밖에 없다.

AOP(Asepct-Oriented Programming)

이 문제를 해결하는 방법이 바로 “관심지향 프로그래밍(AOP) 기법” 이다.

  • 횡단 관심을 수행하는 코드 -> Aspect 라는 특별한 객체로 모듈화

  • weaving 이라는 작업을 통해 모듈화한 코드를 핵심 기능에 끼워넣기

  • 기존 코드 변경 x

  • 시스템 핵심 기능에서 필요한 부가 기능을 효과적으로 이용

  • 횡단 관심에 변경이 생긴다 -> 해당 Aspect 수정

AOP와 관련된 용어 설명

  • 조인 포인트(Joinpoint)

애플리케이션 실행 중의 특정한 지점

ex) 메서드 호출, 클래스 초기화, 객체 생성 시점, etc

  • 어드바이스(Advice)

특정 조인포인트에 실행하는 코드

ex) Before Advice, After Advice, etc

Before, After 기준은 JoinPoint

  • 포인트컷(Pointcut)

여러 조인포인트의 집합체

언제 어드바이스 실행할지 정의할 때 사용

애플리케이션 구성 요소에 어드바이스를 어떻게 적용할지 상세하게 제어 가능

  • 애스펙트(Aspect)

어드바이스와 포인트컷을 조합한 것.

애플리케이션이 가져야 할 로직과 그것을 실행해야 하는 지점을 정의한 것.

  • 위빙(Weaving)

애스펙트를 실제로 주입하는 과정

컴파일 시점 AOP 솔루션은 이 작업을 컴파일 시점에 하며 빌드 중에 별도의 과정을 거친다.

실행 시점 AOP 솔루션은 실행 중에 동적으로 위빙이 일어난다.

개방-폐쇄 원칙(Open-Closed Principle,OCP)

“기존의 코드를 변경하지 않으면서 기능을 추가할 수 있도록 설계가 되어야 한다”

OCP 설계할 때 가장 중요한 점

  • 무엇이 변하는 것인지

  • 무엇이 변하지 않는 것인지

이 두개를 명확히 구분해야 한다.

변해야 하는 것은 쉽게 변할 수 있게 하고,

변하지 않아야 할 것은 변하는 것에 영향을 받지 않게 해야 한다.

개방-폐쇄 원칙에서 자주 사용되는 문법은 Interface이다.

Interface를 이용해 다른 클래스들이 기능들을 override 할 수 있게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface car{
  public void go();
}

class bmw implements car{
  public void go(){
      System,out.println("i'm bmw");
  }
}

class audi implements car{
  public void go(){
      System,out.println("i'm audi");
  }
}

이렇게 car 인터페이스의 go 메소드를 override 한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CarName{
  private car whatcar;
  
  public void setCar(car whatcar){
    this.whatcar = whatcar;
  }
}
class Client{
  public static void main(String[] args){
    CarName cn = new CarName();
    cn.setCar(new bmw());
    cn.setCar(new audi());
    cn.go();
  }
}

이런식으로 인터페이스를 설계한다.

이렇게 되면, 변하기 쉽게 만들어지면서

interface를 상속받아 구현된 클래스들은 변하지 않는다.

  • 단위 테스트 = 빠른 테스트

  • 모의 객체 = 테스트용 가짜 객체

더미 객체 : 테스트할 때 객체만 필요하고 해당 객체의 기능까지는 필요하지 않는 경우

테스트 스텁 : 더미 객체에 단순한 기능 추가

테스트 스파이 : 주로 테스트 대상 클래스가 의존하는 클래스로의 출력을 검증

가짜 객체 : 실제 의존 클래스의 기능을 대체해야 할 경우에 사용

목 객체 : 미리 정의한 기대 값과 실제 호출을 단언문으로 비교

LSP(Liskov Subsititution Principle)

“자식 클래스는 부모클래스에서 가능한 행위를 수행할 수 있어야 한다”

LSP를 만족하면 프로그램에서 부모 클래스의 인스턴스 대신에 자식 클래스의 인스턴스로 대체해도

프로그램의 의미 변화 x

LSP는 부모 클래스와 자식 클래스 사이의 행위가 일관성이 있어야 한다.

예를 들어 보자.

포유류 클래스와 원숭이 클래스가 있다.

원숭이 클래스는 포유류 클래스의 상속을 받고 있다.

  • 포유류는 알을 낳지 않고 새끼를 낳아 번식한다.

  • 포유류는 젖을 먹여서 새끼를 키우고 폐를 통해 호흡한다.

  • 포유류는 체온이 일정한 정온 동물이며 털이나 두꺼운 피부로 덮여 있다.

일반화 관계를 확인하는 방법 = 단어를 바꿔보자!!!

  • 원숭이는 알을 낳지 않고 새끼를 낳아 번식한다.

  • 원숭이는 젖을 먹여서 새끼를 키우고 폐를 통해 호흡한다.

  • 원숭이는 체온이 일정한 정온 동물이며 털이나 두꺼운 피부로 덮여 있다.

전혀 문제가 없어 보인다.

따라서 원숭이와 포유류는 일관성이 있다고 말할 수 있다.

하지만 오리너구리를 넣어보자.

  • 오리너구리는 알을 낳지 않고 새끼를 낳아 번식한다.

  • 오리너구리는 젖을 먹여서 새끼를 키우고 폐를 통해 호흡한다.

  • 오리너구리는 체온이 일정한 정온 동물이며 털이나 두꺼운 피부로 덮여 있다.

오리너구리는 포유류지만, 알을 낳아 번식하지 않는다.

문제가 있다!!!!

따라서 포유류 클래스는 LSP를 만족하지 않은 설계라고 할 수 있다.

LSP를 만족시키는 간단한 방법은 override 하지 않는 것이다!

override를 하게 되면 부모와 자식간의 일관성은 깨지게 되기 때문이다.

DIP(Dependency Inversion Principle)

“의존 관계를 맺을 때는 변화하기 쉬운 것 or 자주 변하는 것보다는

변화하기 어려운 것 or 거의 변화가 없는 것에 의존하라”

쉬운 것, 어려운 것 어떻게 구분?

정책, 전략과 같은 어떤 큰 흐름이나 개념 같은 추상적인 것 = 어려운 것

구체적인 방식, 사물 등과 같은 것 = 쉬운 것

으로 구분하면 좋다!!

예를 들어 보자

내가 차를 타는 경우를 생각해 보자!!

차에는 Audi, BMW 등등이 있다.

그럼 내가 탈 차는 Audi or BMW or etc 이므로 변화기 쉽다.

하지만 차를 탄다는 경우 자체는 변하기 어렵다!!!

따라서 DIP 설계를 만족하려면

어떤 클래스가 도움을 받을 때 구체적인 클래스보다는 인터페이스나 추상 클래스와 의존 관계 맺도록 설계!!

ISP(Interface Segregation Principle)

내가 영어, 수학, 정치를 잘한다고 가정해보자.

내가 수학 선생님이 된다면 수학 능력을 사용할 것이고

내가 영어 선생님이 된다면 영어 능력을 사용할 것이고

내가 정치인이 된다면 정치 능력을 사용할 것이다.

만약 영어 능력이 변한다면

수학 능력이나 정치 능력에는 영향을 미치지 않을 확률이 높다.

하지만 영어 능력에는 영향을 미칠 수 있다.

이게 바로 ‘인터페이스 분리 원칙’이다.

“자신이 이용하지 않는 기능에는 영향을 받지 않아야 한다”

  • ISP는 인터페이스를 클라이언트에 특화되도록 분리시키는 설계 원칙이다