_'소프트웨어 개체(artifact)는 확장에는 열려 있어야 하고, 변경에는 닫혀 있어야 한다.'
- 소프트웨어 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안된다.
- OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다.
- 이러한 목표를 달성하려면 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야 한다.
- 소프트웨어 아키텍처를 공부하는 가장 근본적인 이유가 바로 이 때문임.
사고 실험
- 재무제표를 웹 페이지로 보여주는 시스템이 있다.
- 새로운 요청 : 이해관계자가 동일한 재무제표 정보를 보고서 형태로 변환해서 흑백 프린터로 출력 해달라는 새로운 요청을 한다.
- 어떻게 하면 될까?
-
(1) 단일 책임 원칙(SRP) 적용 : 서로 다른 목적으로 변경되는 요소를 적절하게 분리한다.
이미지 출처 : https://wedonttalknemore.tistory.com/14?category=967824
-
여기서 가장 중요한 것은 보고서 생성이 두 개의 책임으로 분리된다는 사실이다.
- financial analyzer(재무 분석기) : 보고서용 데이터를 계산하는 책임
- Web Reporter / Print Reporter : 이 데이터를 웹으로 보여주거나 종이로 프린트하기에 적합한 형태로 표현하는 책임
-
(2) 의존성 역전 원칙(DIP) 적용 : 이들 요소 사이의 의존성을 체계화함으로써 변경량을 최소화할 수 있다.
- 책임을 분리하면 두 책임 중 하나에서 변경이 발생하더라도 다른 하나는 변경되지 않도록 소스 코드 의존성도 확실히 조직화해야 한다.
- 또한 새로 조직화한 구조에서는 행위가 확장될 때 변경이 발생하지 않음을 보장해야 한다. → 처리 과정을 클래스 단위로 분할하고, 이들 클래스를 아래 그림에서 이중선으로 표시한 컴포넌트 단위로 구분해야 한다. 이미지 출처 : https://wedonttalknemore.tistory.com/14?category=967824
- 클래스 설명
- I : 인터페이스
- DS : 데이터 구조
- 열린 화살표 : 사용(Using) 관계
- 닫힌 화살표 : 구현(implement) 관계 또는 상속(inheritance) 관계
- 2가지 주목할 점 (1) 모든 의존성이 소스 코드 의존성을 나타낸다는 사실이다.
-
화살표가 A 클래스에서 B 클래스로 향한다면, A 클래스에서는 B 클래스를 호출하지만 B 클래스에서는 A 클래스를 전혀 호출하지 않음.
(2) 이중선은 화살표와 오직 한 방향으로만 교차한다.
-
모든 컴포넌트 관계는 단 방향으로 이루어진다.
-
이들 화살표는 변경으로부터 보호하려는 컴포넌트를 향하도록 그려진다.
- A 컴포넌트에서 발생한 변경으로부터 B 컴포넌트를 보호하려면 반드시 A 컴포넌트가 B 컴포넌트에 의존해야 한다.
이미지 출처 : https://wedonttalknemore.tistory.com/14?category=967824
-
여기서는 Presenter에서 발생한 변경으로부터 Controller를 보호하고자 한다.
-
View에서 발생한 변경으로부터 Presenter를 보호하고자 한다.
-
Interactor는 다른 모든 것에서 발생한 변경으로부터 보호하고자 한다.
-
Interactor는 OCP를 가장 잘 준수할 수 있는 곳에 위치한다.
- Interactor는 이처럼 왜 특별한 위치에 있어야 하는가?
- Interactor가 업무 규칙을 포함하기 때문이다. (가장 중요한 문제는 Interactor가 담당)
- Interactor는 애플리케이션에서 가장 높은 수준의 정책을 포함한다. 다른 컴포넌트들은 모두 주변적인 문제를 처리한다.
- Interactor 입장에서 Controller는 부수적이지만 , Controller는 Presenter와 View에 비해서는 중심적인 문제를 담당한다. (마찬가지로 Presenter > View)
- 컴포넌트 수준(level)
- 이처럼 컴포넌트 보호의 계층구조가 생김으로써 각 컴포넌트의 수준이 정해진다.
- Interactor: 가장 높은 수준의 개념 → 최고의 보호를 받음
- View : 가장 낮은 수준의 개념 → 거의 보호를 받지 못함
- Presenter : View 보다는 높고, Controller나 Interactor 보다는 낮은 수준
-
아키텍처 수준에서 OCP가 동작하는 방식
- 이러한 보호의 계층 구조는 아키텍처 수준에서 OCP가 동작하는 방식을 보여준다.
- 아키텍트는 기능이 어떻게(how), 왜why, 언제when 발생하는지에 따라서 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화한다.
- 컴포넌트 계층구조를 이와 같이 조직화하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.
방향성 제어
- FinancialDataGateway 인터페이스는 FinancialReportGenerator와 FinancialDataMapper 사이에 위치. → 의존성을 역전시키기 위함.
FinancialDataGateway 인터페이스가 없었다면 의존성이 Interactor 컴포넌트에서 Database 컴포넌트로 바로 향하게 된다.
정보 은닉
- FinancialReportRequester 인터페이스는 방향성 제어와는 다른 목적을 가짐.
- 이 인터페이스는 FinancialReportController가 Interactor 내부에 대해 너무 많이 알지 못하도록 막기 위해 존재.
- 만약 이 인터페이스가 없었다면, Controller는 FinancialEntities에 대해 추이 종속성(transtive dependency)을 가지게 된다.
- 추이 종속성을 가지게 되면, 소프트웨어 엔티티는 '자신이 직접 사용하지 않는 요소에는 절대로 의존해서는 안 된다'는 소프트웨어 원칙을 위반하게 된다.
- Controller에서 발생한 변경으로부터 Interactor를 보호하는 일의 우선순위가 가장 높지만, 반대로 Interactor에서 발생한 변경으로부터 Controller도 보호되기를 바란다. 이를 위해 Interactor 내부를 은닉한다.