이론/설계

아키텍처 - 설계원칙 (SOLID)

tsyang 2021. 3. 21. 23:52

SRP (단일 책임 원칙)


 

흔히 이걸 모듈이 단 하나의 일만을 해야 한다는 의미로 오인하기 쉬운데 그렇지 않다. (단, 함수는 단 하나의 일을 해야한다는 원칙이 있긴 한데 이건 설계원칙은 아니고 리팩토링할때나 쓰는 원칙)

 

SRP의 진정한 의미는 '단일 모듈은 변경의 이유가 오직 하나뿐이어야 한다.' 이다.

다시 말하면 '단일 모듈은 그 모듈을 변경하려는 집단(액터)이 하나여야 한다.' 

 

만약 한 집단의 수정사항을 반영한 것이 다른 집단의 작업에 영향을 끼칠 수 있다면 이것이 SRP를 위반한 사례라고 볼 수 있다. (이런 사례는 종종 형상관리툴에서 Merge를 발생시킨다.)

 

해결책은 무엇일까? 확실한 방법은 액터마다 데이터와 메서드를 분리하고 데이터는 별도의 클래스를 만들어 공유하는 방식이다. 그러나 이런 방법은 여러 클래스들을 추적하는데 어려움이 생길 수 있다는 단점이 있는데 이 경우 퍼사드(Facade 패턴)으로 보완할 수 있다. 

 

단일 책임 원칙은 메서드와 클래스 수준의 원칙이다.

 

 

OCP 개방 폐쇄 원칙


 

'소프트웨어 개체는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.' 라는 원칙이다. 즉, 개체의 행위는 확장할 수 있지만, 이때 변경이 발생하면 안 된다.

 

뭔 소릴까? 예를 들어 보자. 모니터로 화면을 보는 게임이 있다. 그리고 이제 이걸 VR버전으로도 출시하기로 한다.(개체의 행위를 확장) OCP원칙을 지켰다면 변경이 없을(최소화될) 것이다.

 

그럼 어떻게 할까? 서로 다른 목적으로 변경되는 요소를 분리하고 (SRP) 요소들 사이의 의존성을 체계화하여(DIP) 모든 컴포넌트의 관계를 단방향으로 만들어 저수준 컴포넌트로부터 고수준 컴포넌트를 보호한다.

 

이렇게 말이다. Interactor는 유저 인풋에대한 반응 등의 게임의 룰을 규정하는 컴포넌트이다.

 

여기서 화살표는 의존성이다. 컴포넌트들은 변경이 되더라도 의존하는 컴포넌트에 영향을 주지 않는다. (즉, 화살표가 가리키는 애들은 변경으로부터 안전하다.) 즉, Controller가 변경되면 Presenter와 View는 영향을 받겠지만 Interactor는 영향을 받지 않는다. Interactor는 OCP를 가장 잘 준수할 수 있는 곳에 위치되어야 하는데, 그도 그럴 것이 DB, Controller, Presenter, View가 게임의 룰에 영향을 주어서는 안되기 때문이다. 즉 Interactor가 가장 중요한 문제를 담당하고, 가장 많은 보호를 받는다. 그리고 그다음은 Controller가 보호를 많이 받고 그 아래로 Presenter, View 순으로 보호 수준이 달라 질 것이다. 이렇게 되면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다. 다시 말해 VR을 지원하는 것(저수준 컴포넌트 추가)이 고수준 컴포넌트에 변경을 야기하지 않는다는 것이다. 

 

 

LSP 리스코프 치환 원칙


 

하위 타입 : A타입의 객체 o1 과 B 타입 객체 o2가 있다. B타입을 이용해서 정의한 프로그램에서 o2 의 자리에 o1을 치환하더라도 프로그램의 행위가 변하지 않으면 A는 B의 하위 타입이다.

 

그러니까 위의 규칙이 안 지켜지는데 하위타입이면 잘못됐다는 거다. 이거는 유명한 Square/Rectangle 예제가 있는데 쓰기 귀찮다. 

 

LSP는 클래스 뿐 아니라 아키텍쳐 관점에서도 적용되어야 한다.

 

ISP 인터페이스 분리 원칙


 

A,B,C 메서드가 정의된 인터페이스 I가 있다. 그리고 클래스 a,b,c가 있고 각각 I의 A,B,C만을 사용한다. 이때 I의 B메서드를 수정하면 b클래스 뿐 아니라 a,c클래스도 다시 컴파일한 후 배포해야 한다. 그래서 이걸 분리하라는게 ISP 원칙이다. 

 

사실 ISP는 언어에 따라 다를 수 있는데 앞의 사례에서 동적 언어에서는 런타임에 추론이 생겨서 소스코드간 의존성이 없기 때문이다. 

 

ISP는 결국 불필요한 무언가가 있는 모듈에 의존하지 말자는 것..

 

DIP 의존성 역전 원칙


 

고수준 정책을 구현하는 코드는 저수준 세부사항을 구현하는 코드에 의존해서는 안된다. 저수준이 고수준에 의존해야 한다.

 

다르게 말하면 추상에 의존하며 구체에는 의존하지 말자는 것.

 

사실 위의 말은 현실적으로 어려운데 왜냐면 String 클래스같은건 구체이지만 현실적으로 반드시 의존해야 하는 클래스이기 때문이다. 대신에 String클래스같은건 매우 안정적이다. (변경이 거의 없다.) 즉, 구체더라도 안정적이면 (변경이 거의 없으면) 의존해도 된다. 이와 마찬가지로 OS나 플랫폼같이 안정성이 보장된 환경에 대해서는 의존해도 된다.

 

추상 인터페이스가 변경되면 이를 구체화한 구현체들도 변경되어야 한다. 그러나 그 반대는 다르다. 따라서 인터페이스는 구현체보다 변동성이 적다. 

 

인터페이스의 변동성을 낮추기 위해 애를 써야한다. 인터페이스를 변경하지 않고도 구현체에 기능을 추가할 수 있는 방법을 찾기위해 노력해야 한다.

 

그러면 어떻게 변동성이 큰 것(구체)에 의존하는 것을 지양하고 변동성이 적은 것(추상)에 의존하는 아키텍처를 짤 수 있을까.

 

변동성이 큰 구체 클래스를 참조(상속)하지 말자 : 대신 추상 인터페이스를 참조하자. 이 규칙은 객체 생성 방식을 제약하며 일반적으로 추상 팩토리를 사용하도록 강제된다. 상속은 신중하게 쓰자. 코드를 변경하기 어렵게 만든다.

 

구체 함수를 오버라이드 하지 말자 : 이건 소스코드 의존성 때문인데 구체 함수 오버라이드 하면 의존성을 상속하는 꼴이기 때문이다. 차라리 추상 함수로 선언하는 것을 고려해야 한다.

 

 

 

'이론 > 설계' 카테고리의 다른 글

클린코드 6장 (객체와 자료 구조)  (0) 2021.05.10
클린코드 4,5장 (주석, 형식)  (0) 2021.05.07
클린 코드 2,3장 (이름, 함수)  (2) 2021.05.02
클린 아키텍쳐  (0) 2021.04.25
아키텍쳐 - 프로그래밍 패러다임  (0) 2021.03.09