Ch9 유연한 설계

본문


들어가며

8장에서 우리는 올바른 객체지향 설계란 의존성 관리임을 학습했다.

의존성 관리독립적인 컨텍스트에 의존함으로써 결합도를 낮추고 변경의 영향 범위를 통제하며 재사용 가능한 설계를 얻기 위해 반드시 고려해야하는 설계의 핵심 요소이다.

그리고 올바른 의존성 관리를 위해 아래와 같은 소주제를 학습했다.

  • 추상화에 의존하라

  • 인터페이스에 의존성을 명시적으로 드러내라

  • 생성과 사용의 책임을 분리하라

이번 장에서는 이와같은 내용을 원칙(개방-폐쇄 원칙, 의존성 역전 원칙)이라는 관점에서 한번 더 정리한다.


핵심.

  • 확장 가능하고 변화에 유연하게 대응할 수 있는 설계를 만들 수 있는 원칙 중 하나는 개방-폐쇄 원칙 이다.

  • 소프트웨어 개체(클래스, 모듈, 함수 등)은 확장에 대해서 열려있어야 하고 수정에 대해서 닫혀있어야 한다.

  • 개방-폐쇄 원칙은 런타임 의존성과 컴파일타임 의존성에 관한 이야기다.

  • 개방-폐쇄 원칙을 수용하는 코드는 컴파일타임 의존성을 수정하지 않고도 런타임 의존성을 쉽게 변경할 수 있다.

  • 의존성의 관점에서 개방-폐쇄 원칙을 따르는 설계란 컴파일타임 의존성을 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조 라고 할 수 있다.

  • 개방-폐쇄 원칙의 핵심은 ‘추상화’에 의존하는 것이다.

  • 추상화핵심적인 부분(공통적인 부분, 변경되지 않는 부분)만을 남기고 불필요한 부분(변경되는 부분, 세부적인 부분)을 생략함으로써 복잡성을 극복하는 방법이다.

  • 추상화 과정을 거치게 되면 문맥이 바뀌더라도 변하지 않는 부분만 남게 되고 문맥에 따라 변하는 부분은 생략된다.

  • 추상화를 사용하면 생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있다.

  • 공통적인 부분은 문맥에 바뀌더라도 변하지 않아야 한다. 다시말해서 수정할 필요가 없어야 한다.

  • 단순히 어떤 개념을 추상화했다고 해서 수정에 닫혀잇는 설계를 만들 수 있는 것은 아니다. 개방-폐쇄 원칙에서 폐쇄를 가능하게 하는 것은 의존성의 방향이다. 수정에 대한 영향을 최소화 하기 위해서는 모든 요소가 추상화에 의존해야 한다.

  • 명시적 의존성의존성 해결을 통해 컴파일타임 의존성을 런타임 의존성으로 대체함으로써 실행 시에 객체의 행동을 확장할 수 있다.

  • 핵심은 추상화라는 것을 기억하라.

  • 올바른 추상화를 설계하고 추상화에 대해서만 의존하도록 관계를 제한함으로써 설계를 유연하게 확장할 수 있다.

  • 변경에 의한 파급효과를 최대한 피하기 위해서는 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야 한다.

  • 추상화가 수정에 대해 닫혀있을 수 있는 이유는 변경되지 않을 부분을 신중하게 결정하고 올바른 추상화를 주의 깊게 선택했기 때문이다.

  • 결국 추상화에도 올바른 추상화가 있다.

  • 객체 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다.

  • 유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임인 생성사용을 분리해야 한다.

  • 전체적으로 결합도를 낮추고 재사용성을 높이기 위해 도메인 개념과는 아무런 상관이 없는 가공의 객체에 책임을 할당해야 하는 경우가 빈번하다.

  • 도메인 모델, 도메인 개념은 설계를 위한 중요한 출발점이지만 단지 출발점임을 명시해야한다.

  • 모든 책임을 ‘도메인 객체’에게 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하와 같은 문제가 야기될 가능성이 높다.

  • 설계자의 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당하여 문제를 해결할 수 있다.

  • 도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공물)이라고 부른다.

  • 어떤 행동을 추가하려고 하는데 이 행동을 책임질만한 마땅한 도메인 개념이 존재하지 않는다면 가공 객체를 추가하고 이 객체에게 책임을 할당하라.

  • 객체지향 어플리케이션의 대부분은 가공 객체들이 도메인 개념 객체보다 더 많은 비중을 차지하는 것이 일반적이다.

  • 숨겨진 의존성은 코드를 이해하기 위해 내부 구현을 살펴봐야 한다는 측면에서 ‘캡슐화’ 위반이다.

  • 객체 사이의 협력이 존재할 때 협력의 본질을 담고 있는 것은 상위 수준의 정책이다.

  • 어떤 협력에서 중요한 정책이나 의사결정 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스다.

  • 상위 수준의 클래스는 어떤 식으로든 하위 수준의 클래스에 의존해서는 안된다.

  • 상위 수준의 클래스와 하위 수준의 클래스 모두 추상화에 의존해야 한다. 이를 ‘의존성 역전 원칙’이라고 부른다.

  • 유연하고 재사용 가능한 설계를 원한다면 모든 의존성의 방향이 추상 클래스나 인터페이스와 같은 추상화를 따라야 한다.

  • 구체 클래스는 의존성의 시작점이어야 한다. 의존성의 목적지가 돼서는 안된다.

  • 추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다.

  • 함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다.

  • 인터페이스의 소유권을 서버가 아닌 클라이언트에 위치시키는 것이 중요하다. 이는 객체지향 프레임워크의 모듈 구조를 설계하는 데 가장 중요한 핵심 원칙이다.

  • 유연하고 재사용 가능하며 컨텍스트에 독립적인 설계는 의존성의 방향을 역전시킨다.

  • 상위 수준의 모듈과 하위 수준의 모듈이 모두 추상화에 의존하도록 설계하라.

  • 훌륭한 객체지향 설계를 위해서는 의존성을 역전시켜야 한다. 그리고 의존성을 역전시켜야만 유연하고 재사용 가능한 설계를 얻을 수 있다.

  • 유연한 설계는 유연성이 필요할때만 옳다.


개방-폐쇄 원칙

소프트웨어 개체(클래스, 모듈, 함수 등)은 확장에 대해 열려있어야 하고, 수정에 대해서는 닫혀있어야 한다.

  • 확장에 대해서 열려 있다 : 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 동작을 추가해서 애플리케이션의 기능을 확장할 수 있다.

  • 수정에 대해 닫혀있다. : 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다.

개방-폐쇄 원칙은 유연한 설계가 기존의 코드를 수정하지 않고도 애플리케이션의 동작을 확장할 수 있는 설계라고 이야기한다.

컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라.

개방-폐쇄 원칙은 사실 런타임의존성과 컴파일타임 의존성에 관한 이야기다.

유연하고 재사용 가능한 설계에서 런타임 의존성컴파일타임 의존성은 서로 다른 구조를 가진다.

영화 예매 시스템의 할인 정책의존성 관점에서 다시 살펴보자.

컴파일타임 의존성 관점에서 Movie 클래스는 추상 클래스인 DiscountPolicy에 의존한다.

런타임 의존성 관점에서 Movie 인스턴스는 AmountDiscountPolicyPercentDiscountPolicy 인스턴스에 의존한다.

위 그림에서 알 수 있듯이 Movie의 관점에서 DiscountPolicy에 대한 컴파일타임 의존성과 런타임 의존성은 동일하지 않다.

현재의 설계는 새로운 할인 정책을 추가해서 기능을 확장할 수 있도록 허용한다. 따라서 확장에 대해서는 열려있다.

또한 기존 코드를 수정할 필요 없이 새로운 클래스릋 추가하는 것만으로 새로운 할인 정책을 확장할 수 있다. 따라서 수정에 대해서는 닫혀있다. 이것이 개방-폐쇄 원칙이 의미하는 것이다.

개방-폐쇄 원칙을 수용하는 코드는 컴파일타임 의존성을 수정하지 않고도 런타임 의존성을 쉽게 변경할 수 있다.

의존성 관점에서 개방-폐쇄 원칙을 따르는 설계란 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조라고 할 수 있다.

추상화가 핵심이다.

개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다.

추상화핵심적인 부분(공통적인 부분, 변경되지 않는 부분)만을 남기고 불필요한 부분(세부적인 부분, 변경되는 부분)을 생략함으로써 복잡성을 극복하는 기법이다.

추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남게 되고 문맥에 따라 변하는 부분은 생략된다. 추상화를 사용하면 생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있다.

개방-폐쇄 원칙의 관점에서 생략되지 않고 남겨지는 부분은 다양한 상황에서의 변하지 않는 공통점을 반영한 추상화의 결과물이다.

공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 다시 말해서 수정할 필요가 없어야 한다. 따라서 추상화 부분은 수정에 대해서 닫혀있다. 추상화를 통해 생략된 부분은 확장의 여지를 남긴다.

이해를 돕기 위해 아래 코드(DiscountPolicy)를 살펴보자.

DiscountPolicy추상화다. 추상화 과정을 통해 생략된 부분은 할인 요금을 계산하는 구체적인 방법이다. 우리는 상속을 통해 생략된 부분을 구체화함으로써 할인 정책을 확장할 수 있는 것이다.

여기서 변하지 않는 부분은 고정하고 변하는 부분은 생략하는 추상화 메커니즘이 개방-폐쇄 원칙의 기반이 된다는 사실에 주목하자. 언제라도 추상화의 생략된 부분을 채워넣음으로써 새로운 문맥에 맞게 기능을 확장할 수 있다. 따라서 추상화는 설계의 확장을 가능하게 한다.

개방-폐쇄 원칙에서 ‘폐쇄’를 가능하게 하는 것은 ‘의존성의 방향’이다.

변경에 대한 영향을 최소화하기 위해서는 모든 요소가 추상화에 의존해야 한다.

위 코드에서 볼 수 있듯 Movie는 할인 정책을 추상화한 DiscountPolicy에만 의존한다. 의존성은 변경의 영향을 의미하고 DiscountPolicy는 변하지 않는 추상화라는 사실에 주목하자.

Movie는 안정된 추상화DiscountPolicy에 의존하기 때문에 할인 정책을 추가하기 위해 DiscountPolicy의 자식 클래스를 추가하더라도 영향을 받지 않는다. 따라서 MovieDiscountPolicy수정에 대해 닫혀있다.

추상화는 확장을 가능하게 하고 추상화에 대한 의존은 폐쇄를 가능하게 한다.

앞서 8장에서 설명한 것 처럼 명시적 의존성의존성 해결을 통해 컴파일 타임 의존성런타임 의존성으로 대체함으로써 실행 시에 객체의 행동을 확장할 수 있다.

이런 것들이 가능한 이유의 중심에는 추상화가 있음을 기억하라.

올바른 추상화를 설계하고 추상화에만 의존하도록 관계를 제한함으로써 설계를 유연하게 확장할 수 있다.

여기서 주의할 점은 추상화를 했다고 해서 모든 수정에 대해 설계가 폐쇄되는 것은 아니라는 것이다. 수정에 대해 닫혀있고 확장에 대해 열려있는 설계는 쉽게 얻어지지 않는다.

변경에 대한 파급효과를 최대한 피하기 위해서는 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야만 한다.

추상화가 수정에 대해 닫혀있을 수 있는 이유(변경의 파급효과를 통제할 수 있는 이유)는 변경되지 않을 부분을 신중하게 결정하고 올바른 추상화를 주의깊게 선택했기 때문이라는 사실을 기억하자.

생성 사용 분리

구체 클래스에 의존하는 코드는 동작을 추가하거나 기능을 변경하기 위해서 기존의 코드를 수정하도록 만들기 때문에 개방-폐쇄 원칙을 위반하게 된다.

결합도가 높을수록 개방-폐쇄 원칙을 따르는 구조를 설계하기가 어려워진다. 알아야 하는 지식이 많아지면 결합도도 높아지게 된다. 특히 객체 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다. 객체의 타입과 생성자에 전달해야 하는 인자에 대한 과도한 지식은 코드를 특정한 컨텍스트에 강하게 결합시킨다.

유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다. 하나는 객체 생성에 대한 책임이고 다른 하나는 객체 사용에 대한 책임이다. 한 마디로 말해서 객체에 대한 생성과 사용을 분리해야 한다.

소프트웨어는 의존성을 “연결”하는 단계와 “실행” 단계를 분리해야 한다.

이 경우 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 Client는 이 객체를 사용하도록 만들 수 있다. 이처럼 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부른다.

FACTORY를 사용하면 MovieAmountDiscountPolicy를 생성하는 책임 모두를 FACTORY로 이동할 수 있다. 이제 Client에는 사용과 관련된 책임만 남게 되므로 생성과 관련된 어떤 지식도 가지지 않을 수 있다.

순수한 가공물에 책임 할당하기

책임 할당의 가장 기본이 되는 원칙은 책임을 수행하는데 필요한 정보를 가장 많이 알고있는 정보 전문가에게 할당하는 것 임을 기억 할 것이다. 그리고 도메인 모델정보 전문가를 찾기 위해 참조할 수 있는 일차적인 재료이다. 따라서 필요한 책임을 식별했고 이 책임을 할당할 객체를 찾기 위해서는 도메인 모델 안의 개념 중에서 적절한 후보가 존재하는지 찾아봐야 한다.

그런데 우리는 방금 생성 책임을 할당하기 위해 도메인 모델에 존재하지 않는 FACTORY를 창조했다. FACTORY를 추가한 이유는 순수하게 기술적인 결정이다. 설계 전체적으로 결합도를 낮추고 재사용성을 높이기 위해 도메인 개념에게 할당돼 있던 객체 생성 책임을 도메인 개념과는 아무런 상관이 없는 가공의 객체로 이동시킨 것이다.

종종 도메인 개념을 표현하는 객체에게 책임을 할당하는 것만으로는 부족한 경우가 발생한다. 도메인 모델은 설계를 위한 중요한 출발점이지만 단지 출발점이라는 사실을 기억해야 한다. 실제로 애플리케이션은 데이터베이스 접근을 위한 객체와 같이 도메인 개념을 초월하는 기계적인 개념들을 필요로 할 수 있다.

모든 책임을 ‘도메인 객체’에게 할당하면 ‘낮은 응집도’, ‘높은 결합도’, ‘재사용성 저하’와 같은 심각한 문제에 봉착하게 될 확률이 높다.

즉, 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임으로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 하는 경우가 빈번히 발생한다.

어떤 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념이 없다면 가공의 객체(PURE FABRICATION)를 추가하고 이 객체에게 책임을 할당하라.

이런 측면에서 객체지향이 ‘실세계의 모방’이라는 말은 옳지 않다. 객체지향 애플리케이션은 도메인 개념뿐만 아니라 설계자들이 임의적으로 창조한 인공적인 추상화들을 포함하고 있다.

객체지향 어플리케이션의 대부분은 실제 도메인에서 발견할 수 없는 가공 객체로 가득 차있다.

도메인 개념을 표현하는 객체와 순수하게 창조된 가공의 객체들이 모여 자신의 역할과 책임을 다하고 조화롭게 협력하는 애플리케이션을 설계하라.

  • 먼저 도메인의 본질적인 개념을 표현하는 추상화를 이용해 애플리케이션을 구축
  • 만약 도메인 개념이 만족스럽지 못하다면 주저하지 말고 가공 객체를 창조

도메인을 반영하는 애플리케이션의 구조라는 제약 안에서 실용적인 창조성을 발휘할 수 있는 능력이 필요하다.

도메인 객체에 책임을 할당하는 것이 ‘응집도’, ‘결합도’, ‘재사용성’을 저해한다면 어떻게 해야하는가? 가공의 객체에 매우 응집된 책임을 할당하라. 이들 클래스는 도메인 상에는 존재하지 않지만 전체 설계의 품질을 높이기 위해 설계자의 임이에 따라 추가한 상상속의 가공물이다.

PURE FABRACTION 패턴은 정보 전문가 패턴에 따라 책임을 할당한 결과가 바람직하지 않은 경우 대안으로 사용된다. 어떤 객체가 책임을 수행하는 데 필요한 많은 정보를 가진 정보 전문가 이지만 책임을 할당할 경우 응집도가 낮아지고 결합도가 높아진다면 가공의 객체를 추가해서 책임을 옮기는 것을 고민하라.

도메인 모델에서 출발하여 설계의 유연성을 추가하기 위해 책임을 이리저리 옮기다 보면 PURE FABRACTION을 추가하게 된다는 사실을 알게 된다. FACTORY객체 생성 책임을 할당할 만한 도메인 객체가 존재하지 않을 때 선택할 수 있는 PURE FABRACTION이다. 14장에서 살펴보겠지만 대부분의 디자인 패턴은 PURE FABRICATION을 포함한다.

숨겨진 의존성은 나쁘다.

우리는 인터페이스에 의존성을 명시적으로 드러내고 의존성 주입을 통해 의존성을 해결할 수 있다는 것을 알고있다. 만일 의존성을 명시적으로 드러내지 않고 코드 내부에 숨긴다면 어떤 일이 발생할까?

의존성을 구현 내부로 감출 경우 의존성과 관련된 문제가 컴파일타임이 아닌 런타임에 가서야 발견된다는 사실을 알 수 있다. 숨겨진 의존성이 이해하기 어렵고 디버깅하기 어려운 이유는 문제점을 발견할 수 있는 시점을 코드 작성 시점이 아니라 실행 시점으로 미루기 때문이다.

숨겨진 의존성캡슐화를 위반한다. 우리는 캡슐화가 필요한 이유는 변경에 대한 영향을 통제하기 위함이라는 것을 알고있다. 그리고 이를 위해 변경되는 불안정한 부분(구현)과 변경되지 않는 안정적인 부분(인터페이스)를 구분하고 변경되는 부분을 안정적인 인터페이스 뒤로 숨기는 것 이라고 배웠다.

만일 어떤 모듈(클래스, 함수 등)을 사용하기 위해 해당 모듈의 구체적인 구현에 대한 지식이 필요하다면 이는 높은 결합도를 갖는 것이고 결국 캡슐화를 위반한 것이다.

이처럼 캡슐화코드를 읽고 이해하는 행위와도 관련이 있다. 클래스의 사용법을 알기 위해서 구현 내부를 샅샅이 뒤져야 한다면 그 클래스의 캡슐화는 무너진 것이다.

숨겨진 의존성이 가지는 가장 큰 문제점은 의존성을 이해하기 위해 코드의 내부 구현을 이해할 것을 강요한다는 것이다.

의존성 주입은 이 문제를 깔끔하게 해결한다. 필요한 의존성은 클래스의 퍼블릭 인터페이스에 명시적으로 드러낸다. 의존성을 이해하기 위해 코드 내부를 읽을 필요가 없기 때문에 의존성 주입은 객체의 캡슐을 단단하게 보호한다.

의존성 역전 원칙

추상화와 의존성 역전

이제 우리는 아래와 같이 Movie를 구현했을 때 어떤 문제가 발생하는지를 예상할 수 있다. Movie구체 클래스에 대한 의존성으로 인해 결합도가 높아지고 재사용성과 유연성이 저해된다.

위 설계가 변경에 취약한 이유는 요금을 계산하는 상위 정책이 요금을 계산하는데 필요한 구체적인 방법에 의존하기 떄문이다. Movie는 가격 계산이라는 더 높은 수준의 개념을 구현한다. 그에 비해 AmountDiscountPolicy는 영화의 가격에서 특정한 금액만큼을 할인해주는 더 구체적인 수준의 메커니즘을 담당하고 있다. 다시말해서 상위 수준의 클래스인 Movie가 하위 수준의 클래스인 AmountDiscountPolicy에 의존하는 것이다.

객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. 다시말해서 어떤 협력에서 중요한 정책이나 의사결정 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스다.

상위 수준의 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준의 클래스가 영향을 받게 될 것이다.

의존성은 변경의 전파와 관련된 것이기 때문에 설계는 변경의 영향을 최소화하도록 의존성을 관리해야 한다. 상위 수준의 클래스는 어떤 식으로든 하위 수준의 클래스에 의존해서는 안되는 것이다.

대부분의 경우 우리가 재사용하려는 대상은 상위 수준의 클래스라는 점을 기억하자. 상위 수준의 클래스가 하위 수준의 클래스에 의존하면 상위 수준의 클래스를 재사용할 때 사우이 수준의 클래스도 필요하기 때문에 재사용하기가 어려워진다.

중요한 것은 상위 수준의 클래스다. 상위 수준의 변경에 의해 하위 수준이 변경되는 것은 납득할 수 있지만 하위 수준의 변경으로 인해 상위 수준이 변경되는 것은 용납할 수 없다.

이 경우에도 해결사는 추상화다. 상위 수준의 클래스와 하위 수준의 클래스 모두 추상화에 의존하도록 하면 하위 수준 클래스의 변경으로 인해 상위 수준의 클래스가 영향을 받는 것을 방지할 수 있다. 또한 상위 수준을 재사용할 때 하위 수준의 클래스에 얽매이지 않고도 다양한 컨텍스트에서 재사용이 가능하다.

이것이 MovieAmountDiscountPolicy 사이에 DiscountPolicy가 자리잡고 있는 이유다. 다시말해서 상위 수준의 클래스와 하위 수준의 클래스 모두 추상화에 의존한다.

가장 중요한 조언은 추상화에 의존하라는 것이다. 유연하고 재사용 가능한 설계를 원한다면 모든 의존성의 방향이 추상클래스나 인터페이스와 같은 추상화를 따라야 한다. 구체 클래스는 의존성의 시작점이어야 한다. 의존성의 목적지가 되어서는 안된다.

  1. 상위 수준의 모듈하위 수준의 모듈에 의존해서는 안된다. 둘 모두 추상화에 의존해야 한다.
  2. 추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다.

이를 의존성 역전 원칙이라고 부른다.

의존성 역전 원칙과 패키지

역전의존성의 방향뿐 아니라 인터페이스의 소유권에도 적용된다. 할인 정책과 관련된 패키지 구조가 아래와 같다고 가정해보자.

위 그림의 설계는 개방-폐쇄 원칙을 준수할 뿐 아니라 의존성 역전 원칙또 따르고 있다. 하지만 Movie를 다양한 컨텍스트에서 재사용 하기 위해서는 불필요한 클래스(DiscountPolicy의 구체 클래스)들이 Movie와 함께 배포돼야 한다.

MovieDiscountPolicy에 의존하고 있다는 사실을 주목하자 Movie를 정상적으로 컴파일하기 위해서는 DiscountPolicy 클래스가 필요하다. 사실 코드의 컴파일이 성공하기 위해 함께 존재해야 하는 코드를 정의하는 것이 바로 컴파일타임 의존성이다. 문제는 DiscountPolicy가 포함된 패키지 내에 AmountDiscountPolicyPercentDiscountPolicy도 함께 포함되어 있다는 것이다. 이것은 DiscountPolicy에 의존하기 위해 반드시 같은 패키지에 포함된 AmountDiscountPolicy와 PercentDiscountPolicy 클래스도 함께 존재해야 한다는 것을 의미한다.

이 경우 DiscountPolicy가 포함된 패키지 내의 어떤 클래스가 수정되더라도 패키지 전체가 재배포돼야 한다. 따라서 불필요한 클래스들을 같은 패키지에 두는 것은 전체적인 빌드 시간을 가파르게 상승시킨다.

이를 해결하기 위해 추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다. 또한 함께 재사용될 필요가 없는 클래스들을 별도의 독립적인 패키지에 모아야 한다.

아래의 그림을 보자.

Movie와 추상 클래스인 DiscountPolicy를 하나의 패키지로 모으는 것은 Movie를 특정한 컨텍스트로부터 완벽하게 분리시킨다. Movie를 다른 컨텍스트에서 재사용하기 위해서는 단지 MovieDiscountPolicy가 포함된 패키지만 재사용하면 된다.

의존성 역전 원칙에 따라 상위 수준의 협력을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.

인터페이스의 소유권서버가 아닌 클라이언트에 위치시키는 것은 객체지향 프레임워크의 모듈 구조를 설게하는데 있어 가장 중요한 핵심 원칙이다.

정리하자.

  1. 유연하고 재사용 가능하며 컨텍스트에 독립적인 설계는 전통적인 패러다임이 고수하는 의존성의 방향을 역전시킨다.
  2. 전통적인 패러다임에서는 상위 수준 모듈이 하위 수준 모듈에 의존했다면 객체지향 패러다임에서는 상위 수준의 모듈과 하위 수준의 모듈 모두 추상화에 의존한다.
  3. 전통적인 패러다임에서는 인터페이스하위 수준 모듈에 속했다면 객체지향 패러다임에서는 인터페이스는 상위 수준의 모듈(클라이언트 클래스/객체)에 속한다.

훌륭한 객체지향 설계를 위해서는 의존성을 역전시켜야 한다. 그리고 의존성을 역전시켜야만 유연하고 재사용 가능한 설계를 얻을 수 있다. 이것이 핵심이다.

참고 및 출처

  • 오브젝트