SOLID 원칙
- 함수와 데이터를
클래스
로 배치하는 방법과 이들 클래스
들을 서로 결합하는 방법을 설명한다.
중간 수준
의 구조를 아래와 같이 만들기 위한 목적을 지닌다.
- 변경에 유연한
- 이해하기 쉬운
- 다양하게 쓰일 수 있는 컴포넌트의 기반이 되는
🤔 코드수준
, 중간수준
, 고수준
이란 무엇인가?
코드수준
: 실제 소스 코드
설계 원칙(SOLID)이 중간 수준
의 소프트웨어 구조를 형성하는데 작용한다.
중간수준
: 모듈 수준, (컴포넌트 원칙)
고수준
: 아키텍처
🤔 클래스, 모듈의 개념적 차이는?
- 동일한 개념을 뜻한다.
- 클래스 : 단순히 함수와 데이터를 결합한 집합 (62p)
- 모듈 : 단순히 함수와 데이터로 응집된 집합 (67p)
SRP: 단일 책임 원칙
오해
모듈은 단 하나의 일만 해야한다. ❌
- 함수는 단 하나의 일만 해야한다. ✅
정의
"A class should have only one reason to change"
- SRP에서는 "변경의 이유(reason to change)"가 될 책임(responsibility)을 규정한다.
- "변경의 이유"란 바로 Actor(변경을 요청하는 집단, 사용자와 이해관계자)를 지칭한다.
- 따라서, 아래와 같이 기술될 수 있다.
하나의 클래스은 오직 하나의 Actor에 대해서만 책임을 갖는다.
- 단일 Actor를 책임지는 코드는 응집성(Cohesion)으로 묶인다.
- 서로 다른 Actor가 의존하는 코드가 있다면, 코드를 분리하도록 한다.
OCP: 개방-폐쇄 원칙
정의
"You should be able to extend a classes behavior, without modifying it."
클래스의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안 된다.
- Open for Extension, 확장에는 열려있어야 하지만
- Closed for Modification, 변경에는 닫혀있어야 한다.
변경을 최소화하려면?
- 서로 다른
목적
으로 변경되는 요소
를 적절하게 분리한다. (SRP, 단일 책임 원칙)
목적
: 변경의 이유, Actor의 요구사항
요소
: 클래스의 내용
- 변경되는 요소 사이의 의존성을 체계화한다. (DIP, 의존성 역전 원칙)
Best Practice
- 변경을 최소화하기 위하여 책임을 분리(SRP)하고 행위가 확장될 때 변경이 발생하지 않도록(OCP) 처리 과정을 클래스 단위로 분할하고, 클래스는 컴포넌트 단위로 분리한다.
﹦ (이중선)
: 컴포넌트 단위
- 화살표와 오직 한 방향으로만 교차한다. (컴포넌트 관계는 단방향으로만 이루어진다.)
→ (열린 화살표)
: 사용
⇾ (닫힌 화살표)
: 구현 혹은 상속
- A ⇾ B: A가 B를 상속(혹은 인터페이스를 채택)하여 수직(혹은 수평)확장을 한다.
- 화살표는 변경으로부터 보호하려는 컴포넌트를 향하도록 그려진다.
View
에서 발생한 변경으로부터 Presenter
를 보호하고자 한다.
Presenter
에서 발생한 변경으로부터 Controller
를 보호하고자 한다.
Interactor는
다른 모든 것에서 발생한 변경으로부터 보호하고자 한다.
- 컴포넌트 계층구조를 이와 같이 조직화하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.
인터페이스의 목적
방향성 제어 (의존성 역전)
FinancialReportGenerator
→ FinancialDataGateway<I>
⇽ FinancialDataMapper
정보 은닉
FinancialReportRequester
- Interactor 내부에 대해 너무 많이 알지 못하도록 막기 위해서 존재한다.
- 없을 경우, Controller는 FinancialEntities에 대해
Transitive Dependency
를 가지게 된다.
Transitive Dependency
을 가지게 되면, 소프트웨어 엔티티는 ‘자신이 직접 사용하지 않는 요소에는 절대로 의존해서는 안 된다’는 소프트웨어 원칙을 위반하게 된다.
LSP: 리스코프 치환 원칙
정의
"Derived classes must be substitutable for their base classes."
클래스는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 치환할 수 있어야 한다.
- 사용자(
Billing
)의 행위가 사용하는 타입(License<Interface>
)에 의존하지 않고 있어, 하위 타입들(PersonalLicense
, Business License
)로 치환할 수 있다.
- 타입에 의존하게 된다면, 타입을 치환할 수 없게 된다.
ISP: 인터페이스 분리 원칙
정의
"Make fine grained interfaces that are client specific."
클라이언트별로 세분화된 인터페이스를 만들어라
- 오퍼레이션을 인터페이스 단위로 나눠라.
- 오퍼레이션을 인터페이스 단위로 나누지 않는다면,
- 무관한 오퍼레이션을 변경하더라도 필요치 않은 배포가 필요로 하다.
- 필요 이상으로 많은 것을 포함하는 모듈에 의존하는 것은 불필요한 컴파일·배포를 강제한다.
DIP: 의존성 역전 원칙
정의
"Depend on abstractions, not on concretions."
구체화(Concretion)에 의존하지말고, 추상화(Absraction)에 의존하라.
- 추상화에 의존하며, 구체화에 의존하지 않는 시스템을 유연성이 극대화된 시스템이라고 한다.
- 구체화에 의존하는 경우(
String
)가 존재하지만, 이는 변경되는 일이 거의 없고 엄격하게 통제된다.
- 안정성이 보장되어진 환경에 대해서는 구체화에 대한 의존성을 용납한다.
- 변동성이 큰 구체화에 대한 의존을 피하도록 한다.
- 인터페이스는 구현체보다 변동성이 낮다.
- 안정된 소프트웨어 아키텍처란 변동성이 큰 구현체에 의존하는 일은 지양하고, 안정된 추상 인터페이스를 선호하는 아키텍처이다.
실천법
- 변동성이 큰 구현체를 참조하지 않는다.
- 인터페이스를 참조하라.
- 객체 생성을 Abstract Factory를 사용하도록 강제한다.
- 변동성이 큰 구현체로부터 파생하지 말라.
- 수직 확장을 지양하라.
- 변경이 더욱 어려워진다.
- 구체함수를 overried 하지 말라.
- 소스코드 의존성을 제고할 수 없고, 상속하게 되버린다.
Abstract Factory
- 객체를 생성하려면 해당 객체를 구체적으로 정의한 코드에 대하여 소스코드 의존성이 발생하니, 이때 사용할 수 있도록 한다.
Application
이 Service
인터페이스를 통하여 ConcreteImpl
를 사용한다.
ConcreteImpl
의 인스턴스 생성이 필요하다.
ConcreteImpl
에 대한 소스코드 의존성이 발생하는 것을 막기 위하여 ServiceFactory
를 이용한다.
- 소스코드 의존성은 곡선(아키텍처의 경계)과 교차할 때 모두 Absract Component로 향한다.
- Abstract Component : 고수준의 비즈니스 로직
- Concrete Component : 비즈니스 로직을 다루기 위한 세부사항들
- 의존성은 제어흐름과 반대방향으로 역전되어 흐른다. 이를 Dependency Inversion(의존성 역전)이라 칭한다.