들어가며

지난 의존성 관리하기 포스팅에서 유연하고 재사용 가능한 설계를 위해 바람직한 의존성이 필요하다고 하였다. 그리고 바람직한 의존성을 위해서는 간략하게 3가지가 필요하다.

  1. 구체적인 클래스가 아니라 추상화에 의존하라.
  1. 생성과 사용의 책임을 분리하라.
  1. 의존성을 퍼블릭 인터페이스에 명시적으로 드러내라

이번 장에서는 위에서 언급한 의존성 관리 방법을 원칙과 기법을 이용하여 자세히 알아보자.


개방-폐쇄 원칙

확장 가능하고 변화에 유연하게 대응할 수 있는 설계를 만들 수 있는 원칙 중 하나는 개방-폐쇄 원칙(OCP)이다. 개방-폐쇄 원칙은 다음과 같은 문장으로 요약할 수 있다.

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

여기서 키워드는 확장수정이다. 이 둘은 순서대로 애플리케이션의 동작코드의 관점을 반영한다. 즉, 동작(기능)의 확장에 대해서는 열려있어야 하고 기존 코드의 수정에 대해서는 닫혀있어야 한다.

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

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

개방-폐쇄 원칙유연한 설계란 기존의 코드를 수정하지 않고도 애플리케이션의 기능을 확장할 수 있는 설계 라고 이야기한다. 처음에는 어떻게 코드를 수정하지 않고 동작(기능)을 확장시킬 수 있는가? 라는 의문이 들 것이다. 어떻게 가능할까?

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

사실 개방-폐쇄 원칙은 런타임 의존성과 컴파일타임 의존성에 관한 이야기다. 기억을 되살려보자. 런타임 의존성은 실행 시 협력에 참여하는 객체 사이의 의존성이고 컴파일타임 의존성은 코드에서 드러나는 클래스들 사이의 의존성이다. 그리고 이미 살펴본 바와 같이 유연하고 재사용 가능한 설계에서는 런타임 의존성과 컴파일타임 의존성은 서로 다른 구조를 갖는다.

영화 예매 시스템의 할인 정책을 의존성 관점에서 다시 한번 살펴보자. 컴파일타임 의존성 관점에서 Movie 클래스는 추상 클래스인 DiscountPolicy에 의존한다. 런타임 의존성 관점에서 Movie 인스턴스는 AmountDiscountPolicyPercentDiscountPolicy 인스턴스에 의존한다. 아래 그림에서 알 수 있는 것처럼 Movie의 관점에서 DiscountPolicy에 대한 컴파일 타임 의존성런타임 의존성은 동일하지 않다.

사실 할인 정책 설계는 이미 개방-폐쇄 원칙을 따르고 있따. 앞 장에서 금액 할인 정책비율 할인 정책을 동시에 적용할 수 있게 중복 할인 정책을 추가 했던 기억을 떠올려 보자. 중복 할인 정책을 추가하기 위해 한 일은 DiscountPolicy의 자식 클래스로 OverlappedDiscountPolicy 클래스를 추가한 것 뿐이다. 기존의 Movie, DiscountPolicy, AmountDiscountPolicy, PercentDiscountPolicy 중 어떤 코드도 수정하지 않았다. 즉, 기존의 코드는 전혀 수정하지 않고 클래스를 추가하는 것만으로 중복 할인 정책이 적용된 영화를 구현할 수 있었다.

단순히 새로운 클래스를 추가하는 것만으로 Movie를 새로운 컨텍스트에서 사용되도록 확장할 수 있었던 것이다.

현재의 설계는 새로운 할인 정책을 추가해서 기능을 확장할 수 있도록 허용한다. 따라서 확장에 대해서는 열려있다. 현재의 설계는 기존 코드를 수정할 필요 없이 새로운 클래스를 추가하는 것만으로 새로운 할인 정책을 확장할 수 있다. 따라서 수정에 대해서는 닫혀있다. 이것이 개방-폐쇄 원칙이 의미하는 것이다.

개방-폐쇄 원칙을 수용하는 코드는 컴파일타임 의존성을 수정하지 않고도 런타임 의존성을 쉽게 변경할 수 있다. 아래 그림에서 알 수 있는 것처럼 중복 할인 정책을 구현하는 OverlappedDiscountPolicy 클래스를 추가하더라도 Movie 클래스는 여전히 DiscountPolicy 클래스에만 의존한다. 따라서 컴파일타임 의존성은 변하지 않는다. 하지만 런타임에 Movie 인스턴스는 OverlappedDiscountPolicy 인스턴스와 협력할 수 있따. 따라서 런타임 의존성은 변경된다.

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


추상화가 핵심이다.

개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다. 여기서 추상화의존이라는 두 개념 모두가 중요하다.

추상화핵심적이고 공통적인 부분만 남기고 불필요한 부분은 생략 함으로써 복잡성을 극복하는 기법이다.

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

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

개방-폐쇄 원칙의 관점에서 생략되지 않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물이다. 공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 다시말해서 수정할 필요가 없어야 한다. 따라서 추상화부분은 수정에 대해서 닫혀있다. 추상화를 통해 생략된 부분(세부적인 부분)은 확장의 여지를 남긴다. 이것이 추상화개방-폐쇄 원칙을 가능하게 만드는 이유다.

이해를 돕기 위해 아래의 DiscountPolicy 코드를 보자.

DiscountPolicy는 할인 여부를 판단하여 할인 요금을 계산하는 calculateDiscountAmount 메서드와 조건을 만족할 때 할인된 요금을 계산하는 추상 메서드인 getDiscountAmount 메서드로 구성돼 있다. 여기서 변하지 않는 부분은 할인 여부를 판단하는 로직이고 변하는 부분은 할인 요금을 계산하는 방법이다. 우리는 상속을 통해 생략된 부분을 구체화 함으로써 할인 정책을 확장할 수 있는 것이다.

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

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

Movie는 할인 정책을 추상화한 DiscountPolicy에 대해서만 의존한다. 의존성은 변경의 영향을 의미하고 DiscountPolicy는 변하지 않는 추상화라는 사실에 주목하라. Movie는 안정된 추상화인 DiscountPolicy에 의존하기 때문에 할인 정책을 추가하기 위해 DiscountPolicy의 자식 클래스를 추가하더라도 영향을 받지 않는다. 따라서 MovieDiscountPolicy는 수정에 대하여 닫혀있다.

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

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

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

여기서 주의할 점은 추상화를 했다고 해서 모든 수정에 대해 설계가 폐쇄되는 것은 아니라는 것이다. 수정에 대해 닫혀있고 확장에 대해 열려 있는 설계는 공짜로 얻어지지 않는다. 변경에 의한 파급효과를 최대한 피하기 위해서는 변하는 것과 변하지 않는 것이 무엇인지를 이해하고 이를 추상화의 목적으로 삼아야만 한다.

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


생성과 사용의 분리

Movie가 오직 DiscountPolicy라는 추상화에만 의존하기 위해서는 Movie내부에서 AmountDiscountPolicy와 같은 구체 클래스의 인스턴스를 생성해서는 안된다. 아래 코드에서 Movie의 할인 정책을 비율 할인 정책으로 변경할 수 있는 방법은 단 한 가지 밖에 없다. 바로 AmountDiscountPolicy의 인스턴스를 생성하는 부분을 PercentDiscountPolicy의 인스턴스를 생성하도록 직접 코드를 수정하는 것 뿐이다. 이것은 동작을 추가하거나 변경하기 위해 기존의 코드를 수정하도록 만들기 때문에 개방-폐쇄 원칙을 위반한다.

결합도가 높아질수록 개방-폐쇄 원칙을 따르는 구조를 설계하기가 어려워진다. 알아야 하는 지식이 많으면 결합도도 높아진다. 특히 객체 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다.

객체의 타입과 생성자에 전달해야 하는 인자에 대한 과도한 지식은 코드를 특정한 컨텍스트에 강하게 결합시킨다.

컨텍스트를 바꾸기 위한 유일한 방법은 코드 안에 명시돼 있는 컨텍스트에 대한 정보를 직접 수정하는 것 뿐이다.

물론 객체 생성을 피할수는 없다. 어딘가에서는 반드시 객체를 생성해야 한다. 문제는 객체 생성이 아니라 부적절한 곳에서 객체를 생성한다는 것이 문제다. Movie 코드를 자세히 살펴보면 생성자 안에서는 DiscountPolicy의 인스턴스를 생성하고, calculateDiscountAmount메서드 안에서는 이 객체에게 메시지를 전송한다는 것을 알 수 있다.

메시지를 전송하지 않고 객체를 생성하기만 했다면 아무런 문제가 없었을 것이다. 또는 객체를 생성하지 않고 메시지를 전송하기만 했다면 괜찮았을 것이다. 동일한 클래스 안에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하면 문제가 된다.

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

사용으로부터 생성을 분리하는데 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다. 다시 말해서 Movie의 클라이언트가 적절한 DiscountPolicy 인스턴스를 생성한 후 Movie에게 전달하게 하는 것이다.

조금만 생각해보면 이 방법이 타당하다는 사실을 알 수 있는데 Movie에게 금액 할인 정책을 적용할지, 비율 할인 정책을 적용할지를 알고 있는 것은 그 시점에 Movie와 협력할 클라이언트이기 때문이다. 현재의 컨텍스트에 관한 결정권을 가지고 있는 클라이언트로 컨텍스트에 대한 지식을 옮김으로써 Movie는 특정한 클라이언트에 결합되지 않고 독립적일 수 있다.

아래 글미은 생성에 관한 책임을 Movie의 클라이언트로 옮길 경우의 의존성을 나타낸 것이다. 이 전의 그림에서 MovieAmountDiscountPolicy에 대한 의존성 때문에 금액 할인 정책이라는 구체적인 컨텍스트에 묶여있다. 반면 아래 그림에서는 AmountDiscountPolicy의 인스턴스를 생성하는 책임을 클라이언트에게 맡김으로써 구체적인 컨텍스트와 관련된 정보는 클라이언트로 옮기고 Movie는 오직 DiscountPolicy의 인스턴스를 사용하는 데만 주력하고 있다.

Movie의 의존성을 추상화인 DiscountPolicy로만 제한하기 때문에 확장에 대해서는 열려있으면서도 수정에 대해서는 닫혀 있는 코드를 만들 수 있는 것이다.

FACTORY 추가하기

생성 책임을 Client로 옮긴 배경에는 Movie는 특정 컨텍스트에 묶여서는 안되지만 Client는 묶여도 상관 없다는 전제가 깔려있다. 하지만 Movie를 사용하는 Client도 특정한 컨텍스트에 묶이지 않기를 바란다고 가정해보자.

Client의 코드를 다시 살펴보면 Movie의 인스턴스를 생성하는 동시에 getFee 메시지도 함께 전송한다는 것을 알 수 있다. Client 역시 생성과 사용의 책임을 함께 지니고 있는 것이다.

Movie의 문제를 해결했던 방법과 동일한 방법을 이용하여 이 문제를 해결할 수 있다. Movie를 생성하는 책임을 Client의 인스턴스를 사용할 문맥을 결정할 클라이언트로 옮기는 것이다. 하지만 객체 생성과 관련된 지식이 Client와 협력하는 클라이언트에게까지 새어나가기를 원하지 않는다고 가정해보자.

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

이제 ClientFactory를 사용해서 생성된 Movie의 인스턴스를 반환받아 사용하기만 하면 된다.

FACTORY를 사용하면 MovieAmountDiscountPolicy를 생성하는 책임 모두를 FACTORY로 이동할 수 있다. 이제 Client에서는 사용과 관련된 책임만 남게 되는데 하나는 생성된 Movie를 얻기 위한 것이고 다른 하나는 Movie를 통해 가격을 계산하기 위한 것이다. Client는 오직 사용과 관련된 책임만 지고 생성과 관련된 어떤 지식도 가지지 않을 수 있다.

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

4장에서 책임 할당 원칙을 패턴의 형태로 기술한 GRASP 패턴에 관해 살펴봤다. 책임 할당의 가장 기본이 되는 원칙은 책임을 수행하는 데 필요한 정보를 가장 많이 알고 있는 INFORMATION EXPERT에게 책임을 할당하는 것이다. 도메인 모델INFORMATION EXPERT를 찾기 위해 참조할 수 있는 일차적인 재료다. 어떤 책임을 할당하고 싶다면 제일 먼저 도메인 모델안의 개념 중에서 적절한 후보가 존재하는지 찾아봐야 한다.

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

시스템을 객체로 분해하는데는 크게 두 가지 방식이 존재한다. 표현적 분해행위적 분해가 그것이다.

표현적 분해도메인에 존재하는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것이다. 즉, 도메인 모델에 담겨있는 개념과 관계를 따르며 도메인과 소프트웨어 사이의 표현적 차이를 최소화 하는것을 목적으로 한다. 따라서 표현적 분해는 객체지향 설계를 위한 가장 기본적인 접근법이다.

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

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

이 경우 도메인 개념을 표현한 객체가 아닌 설계자가 편의를 위해 임의로 만들어낸 가공의 객체에게 책임을 할당해서 문제를 해결해야 한다.

어떤 행동을 추가하려고 하는데 이 행동을 책임질 마땅한 도메인 개념이 존재하지 않는다면 PURE FABRICATION을 추가하고 이 객체에게 책임을 할당하라. 그 결과로 추가된 PURE FABRICATION은 보통 특정한 행동을 표현하는 것이 일반적이다. 따라서 PURE FABRICATION표현적 분해보다 행위적 분해에 의해 생성되는 것이 일반적이다.

이런 측면에서 객체지향이 실세계의 모방이라는 말은 옳지 않다. 객체지향 애플리케이션은 도메인 개념 뿐만 아니라 설계자들이 임의적으로 창조한 인공적인 추상화들을 포함하고 있다. 애플리케이션 내에서 인공적으로 창조한 객체들이 도메인 개념을 반영하는 객체들보다 오히려 더 많은 비중을 차지하는 것이 일반적이다.

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

설계자로서의 우리의 역할은 도메인 추상화를 기반으로 애플리케이션 로직을 설계하는 동시에 품질의 측면에서 균형을 맞추는 데 필요한 객체들을 창조하는 것이다. 도메인 개념을 표현하는 객체순수하게 창조된 가공의 객체들이 모여 자신의 역할과 책임을 다하고 조화롭게 협력하는 애플리케이션을 설계하는 것이 목표여야 한다.

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

도메인을 반영하는 애플리케이션의 구조라는 제약 안에서 실용적인 창조성을 발휘할 수 있는 능력은 훌륭한 설계자가 갖춰야 할 기본적인 자질이다.

객치제향 설계는 도메인 상의 개념을 소프트웨어 객체로 구현하고 책임을 할당한다. 하지만 만약 도메인 객체에 책임을 할당할 경우 HIGH COHESION(높은 응집도), LOW COUPLING(낮은 결합도), 재사용성 등의 목적을 위반한다면 어떻게 해야하는가?

도메인 개념을 표현하지 않는, 인위적으로 또는 편의상 만든 클래스에 매우 응집된 책임을 할당하라. 이들 클래스는 도메인 상에는 존재하지 않지만 순수하게 전체 설계의 품질을 높이기 위해 설계자의 임의에 따라 추가한 상상속의 가공물이다.

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

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


의존성 주입

생성과 사용을 분리하면 Movie에는 오로지 인스턴스를 사용하는 책임만 남게 된다. 이것은 외부의 다른 객체가 Movie에게 생성된 인스턴스를 전달해야 한다는 것을 의미한다. 이처럼 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을 의존성 주입이라고 한다.

의존성 주입은 근본적으로 의존성 해결 방법과 관련이 깊다. 의존성 해결은 컴파일 타임 의존성런타임 의존성의 차이점을 해소하기 위한 다양한 메커니즘을 포괄한다. 의존성 주입은 의존성을 해결하기 위해 의존성을 객체의 퍼블릭 인터페이스에 명시적으로 드러내서 외부에서 필요한 런타임 의존성을 전달할 수 있도록 만드는 방법을 포괄하는 명칭이다. 따라서 의존성 주입에는 의존성을 해결하는 세 가지 방법을 가리키는 별도의 용어를 정의한다.

1. 생성자 주입 : 객체를 생성하는 시점에 생성자를 통한 의존성 해결

2. setter 주입 : 객체 생성 후 setter 메서드를 통한 의존성 해결

3. 메서드 주입 : 메서드 실행 시 인자를 이용한 의존성 해결

숨겨진 의존성은 나쁘다.

의존성 주입 외에도 의존성을 해결할 수 있는 다양한 방법이 존재한다. 그 중에서 가장 널리 사용되는 대표적인 방법은 SERVICE LOCATOR 패턴이다. SERVICE LOCATOR는 의존성을 해결 할 객체들을 보관하는 일종의 저장소이다. 외부에서 객체에게 의존성을 전달하는 의존성 주입과 달리 SERVICE LOCATOR의 경우 객체가 직접 SERVICE LOCATOR에게 의존성을 해결해줄 것을 요청한다.

예를들어 ServiceLocator라는 클래스가 SERVICE LOCATOR의 역할을 수행한다고 가정하자. SERVICE LOCATOR 버전의 Movie는 직접 ServiceLocator의 메서드를 호출해서 DiscountPolicy에 대한 의존성을 해결한다.

ServiceLocatorDiscountPolicy의 인스턴스를 등록하고 반환할 수 있는 메서드를 구현한 저장소다. ServiceLocatorDiscountPolicy의 인스턴스를 등록하기 위한 provide 메서드와 인스턴스를 반환하는 discountPolicy 메서드를 구현한다.

Movie의 인스턴스가 AmountDiscountPolicy의 인스턴스에 의존하기를 원한다면 다음과 같이 ServiceLocator에 인스턴스를 등록한 후 Movie를 생성하면 된다.

여기까지만 보면 SERVICE LOCATOR 패턴은 의존성을 해결할 수 있는 가장 쉽고 간단한 도구인 것처럼 보인다. 하지만 개인적으로 SERVICE LOCATOR 패턴은 선호하지 않는다. SERCIE LOCATOR 패턴의 가장 큰 단점은 의존성을 감춘다는 것이다. MovieDiscountPolicy에 의존하고 있지만 Movie의 퍼블릭 인터페이스 어디에도 이 의존성에 대한 정보가 표시돼 있지 않다. 의존성은 암시적이며 코드 깊숙한 곳에 숨겨져 있다.

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

문제의 원인은 숨겨진 의존성이 캡슐화를 위반했기 때문이다. 단순히 인스턴스 변수의 가시성을 private으로 선언하고 변경되는 내용을 숨겼다고 해서 캡슐화가 지켜지는 것은 아니다.

캡슐화는 코드를 읽고 이해하는 행위와 관련이 있다. 클래스의 퍼블릭 인터페이스만으로 사용 방법을 이해할 수 있는 코드가 캡슐화의 관점에서 훌륭한 코드다.

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

의존성 주입은 이 문제를 깔끔하게 해결한다. 필요한 의존성은 클래스의 퍼블릭 인터페이스에 명시적으로 드러난다. 의존성을 이해하기 위해 코드 내부를 읽을 필요가 없기 때문에 의존성 주입은 객체의 캡슐을 단단하게 보호한다. 의존성과 관련된 문제도 최대한 컴파일타임에 잡을 수 있다.

가급적 의존성을 객체의 퍼블릭 인터페이스에 노출하라. 의존성을 구현 내부에 숨기면 숨길수록 코드를 이해하기도, 수정하기도 어려워진다.

어쩔 수 없이 SERVICE LOCATOR 패턴을 사용해야 하는 경우도 있다. 깊은 호출 계층에 걸쳐 동일한 객체를 계속해서 전달해야 하는 고통을 견디기 어려운 경우에는 어쩔 수 없이 SERVICE LOCATOR 패턴을 사용하는 것을 고려하라.


의존성 역전 원칙

추상화와 의존성 역전

객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. MovieAmountDiscountPolicy 사이의 협력이 가지는 본질은 영화의 가격을 계산하는 것이다. 어떻게 할인 금액을 계산할 것인지는 협력의 본질이 아니다. 다시 말해서 어떤 협력에서 중요한 정책이나 의사결정, 비즈니스의 본질을 담고 있는 것은 상위 수준의 클래스(Movie) 이다.

그러나 이런 상위 수준의 클래스하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것이다. 하위 수준의 AmountDiscountPolicyPercentDiscountPolicy로 변경했다고 해서 상위 수준의 Movie가 영향을 받아서는 안된다.

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

이는 재사용성과도 관련이 있다. Movie를 재사용 하기 위해서는 Movie가 의존하는 AmountDiscountPolicy 역시 함께 재사용 해야한다. 대부분의 경우 우리가 재사용하려는 대상은 상위 수준의 클래스라는 점을 기억하라.

상위 수준의 클래스가 하위 수준의 클래스에 의존하면 상위 수준의 클래스를 재사용 할 때 하위 수준의 클래스도 필요하기 때문에 재사용하기가 어려워진다.

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

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

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

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

참고 및 출처

  • 오브젝트