OCP: 개방-폐쇄 원칙
Last updated
Last updated
개방-폐쇄 원칙(OCP)라는 용어는 1988년에 버트란트 마이어가 만들었는데, 다음과 같다.
소프트웨어 개체는 확장에는 열려 있어야하고, 변경에는 닫혀 있어야 한다.
이는 소프트웨어 개체의 행위는 확장할 수 있어야 하지만, 이때 개체를 변경해서는 안 된다는 뜻이다.
만약 요구사항을 확장하는 데 소프트웨어를 많이 수정해야 한다면, 그 소프트웨어 시스템을 설계한 아키텍트는 엄청난 실패를 한 것이다.
재무제표를 웹 페이지로 보여주는 시스템이 있다고 생각해보자.
웹 페이지에 표시되는 데이터는 스크롤할 수 있으며, 음수는 빨간색으로 출력한다.
이것을 이해관계자가 보고서 형태로 변환해서 흑백 프린터로 출력해 달라고 요청했다고 해보자.
바뀌는 부분
데이터 : 재무 데이터 -> 보고서용 재무 데이터
출력 방식 :
웹에 표시
프린터 출력
소프트웨어 아키텍처가 훌륭하다면 변경되는 코드의 양이 가능한 한 최소화될 것이다. 이상적인 변경량은 0이다.
이는 서로 다른 목적으로 변경되는 요소를 적절하게 분리하고( 단일 책임 원칙), 이들 요소 사이의 의존성을 체계화함으로써(의존성 역전 원칙) 변경량을 최소화할 수 있다.
여기서 중요한 것은 보고서 생성이 두 개의 책임으로 분리된다는 사실이다.
하나는 보고서용 데이터를 계산하는 책임이며, 나머지 하나는 이 데이터를 웹으로 보여주거나 종이로 프린트하기에 적합한 형태로 표현하는 책임이다.
위 처럼 책임을 분리했다면, 두 책임 중 하나에서 변경이 발생하더라도 다른 하나는 변경되지 않도록 소스 코드 의존성도 확실히 조직화해야 한다.
또한, 새로 조직화한 구조에서는 행위가 확장될 때 변경이 발생하지 않음을 보장해야 한다.
위 목적을 달성하려면 처리 과정을 클래스 단위로 분할하고, 이들 클래스를 아래 그림처럼 컴포넌트 단위로 구분해야 한다.
Controller - 좌측 상단
Interactor - 우측 상단
Dababase - 우측 하단
Presenter & View - 좌측 하단
<I>로 표시된 클래스는 인터페이스이며, <DS>로 표시된 클래스는 데이터 구조다.
화살표가 열려 있다면 사용 관계이며, 닫혀 있다면 구현관계 또는 상속 관계다
여기에서 모든 의존성이 소스 코드 의존성을 나타내도 있다.
예를 들어 화살표가 A 클래스에서 B클래스로 향한다면, A 클래스 에서는 B 클래스를 호출하지만 B 클래스에서는 A 클래스를 전혀 호출하지 않음을 뜻한다.
여기서 주목해야 할 또 다른 점은 이중선은 화살표와 오직 한 방향으로만 교차한다는 사실이다.
A 컴포넌트에서 발생한 변경으로부터 B 컴포넌트를 보호하려면 반드시 A 컴포넌트가 B 컴포넌트에 의존해야 한다.
위 예의 경우 Presenter에서 발생한 변경으로 부터 Controller를 보호하고자 한다.
그리고 VIew에서 발생한 변경으로부터 Presenter를 보호하고자 한다.
Interactor는 다른 모든 것에서 발생한 변경으로부터 보호하고자 한다.
Interactor는 OCP를 가장 잘 준수할 수 있는 곳에 위치한다. Database, Controller, Presenter, View에서 발생한 어떤 변경도 Interactor에 영향을 주지 않는다.
그 이유는 바로 interactor가 업무 규칙을 포함하기 때문이다.
Interactor는 애플리케이션에서 가장 높은 수준의 정책을 포함한다. Interactor 이외의 컴포넌트는 모두 주변적인 문제를 처리한다. 가장 중요한 문제는 Interactor가 담당한다.
Interactor 입장에서는 Controller가 부수적이지만, Controller는 Presenter와 View에 비해서는 중심적인 문제를 담당한다.
마찬가지로 Presenter가 Controller보다는 부수적이더라도 View보다는 중심적인 문제를 처리한다.
보호의 계층 구조가 '수준(level)'이라는 개념을 바탕으로 어떻게 생성되는지 주목하자.
Interactor는 가장 높은 수준의 개념이며, 따라서 최고의 보호를 받는다.
View는 가장 낮은 수준의 개념 중 하나이며, 따라서 거의 보호를 받지 못한다.
이것이 바로 아키텍처 수준에서 OCP가 동작하는 방식이다.
아키텍트는 기능이 어떻게, 왜, 언제 발생하는지에 따라서 기능을 분리하고, 분리한 기능을 컴포넌트의 계층구조로 조직화한다.
컴포넌트 계층구조를 이와같이 조직화하면 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있다.
위 예시에서 FinancialDataGatewat 인터페이스는 FinancialReportGenerator와 FinancialDataMapper 사이에 위치하는데, 이는 의존성을 역전시키기 위해서다.
FinancialDataGateway 인터페이스가 없었다면, 의존성이 Interactor 컴포넌트에서 Database 컴포넌트로 바로 향하게 된다.
FinalcialReport Presenter 인터페이스와 2개의 View 인터페이스도 같은 목적을 가진다.
FinancialReportRequester 인터페이스는 방향성 제어와는 다른 목적을 가진다.
이 인터페이스는 FinancialReportController가 Interactor 내부에 대해 너무 많이 알지 못하도록 막기 위해서 존재한다. 만약 이 인터페이스가 없었다면, Controller는 FinancialEntities에 대해 추이 종속성을 가지게 된다.
추이 종속성을 가지게 되면, 소프트웨어 엔티티는 '자신이 직접 사용하지 않는 요소에는 절대로 의존해서는 안 된다'는 소프트웨어 원칙을 위반하게 된다.
이 원칙은 인터페이스 분리 원칙(ISP)와 공통 재사용 원칙(CRP)을 설명할 때 다시 한번 설명한다.
다시 말해, Controller에서 발생한 변경으로부터 Interactor를 보호하는 일의 우선순위가 가장 높지만,
반대로 Interactor에서 발생한 변경으로부터 Controller도 보호되기를 바란다. 이를 위해 Interactor 내부를 은닉한다.
OCP는 시스템의 아키텍처를 떠받치는 원동력 중 하나다.
OCP의 목표는 시스템을 확장하기 쉬운 동시에 변경으로 인해 시스템이 너무 많은 영향을 받지 않도록 하는 데 있다.
이를 위해 시스템을 컴포넌트 단위로 분리하고, 저수준 컴포넌트에서 발생한 변경으로부터 고수준 컴포넌트를 보호할 수 있는 형태의 의존성 계층구조가 만들어지도록 해야 한다.
˙추이 종속성 :
클래스 A가 클래스 B에 의존하고, 다시 클래스 B가 클래스 C에 의존한다면, 클래스 A는 클래스 C에 의존하게 된다.
이를 추이 종속성이라고 부른다.
클래스 이외의 소프트웨어의 모든 엔티티에도 동일하게 적용된다.
만약 클래스 의존성이 순환적이라면, 모든 클래스가 서로 의존하게 되는 문제가 있다.