이 장에서는 8장에서 살펴본 다양한 의존성 관리 기법들을 원칙이라는 관점에서 정리한다.
소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려 있어야 하고, 수정에
대해서는 닫혀 있어야 한다.
여기서 키워드는 '확장'과 '수정'으로, 애플리케이션의 '동작'과 '코드'의 관점을 반영한다.
유연하고 재사용 가능한 설계에서 런타임 의존성과 컴파일타임 의존성은 서로 다른 구조를 가진다.

위같은 설계에서 새 중복 할인 정책을 추가할 경우, 기존 코드를 변경할 필요가 없다.
단순히 새로운 클래스를 추가하는 것만으로 Movie를 새로운 컨텍스트에서 사용되도록
확장할 수 있다.
새로운 할인 정책을 추가해서 기능을 확장할 수 있도록 허용한다. 따라서 확장에 대해서는
열려 있다. 기존 코드를 수정할 필요 없이 새 클래스를 추가하는 것만으로 새로운 할인 정책을
확장할 수 있다. 따라서 수정에 대해서는 닫혀 있다. 이것이 OCP가 의미하는 것이다.
의존성 관점에서 OCP를 따르는 설계란 컴파일타임 의존성은 유지하면서 런타임 의존성의
가능성을 확장하고 수정할 수 있는 구조라고 할 수 있다.
OCP의 핵심은 추상화에 의존하는 것이다.
추상화란 핵심적인 부분만 남기고 불필요한 부분을 생략해 복잡성을 극복하는 기법이다.
이 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남고, 문맥에 따라 변하는 부분은
생략된다. 생략된 부분을 문맥에 맞게 채워넣어 각 문맥에 적합하게 기능을 구체화,
확장할 수 있다.
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList<>();
public DiscountPolicy(DiscountCondition...conditions) {
this.conditions = Arrays.asList(conditions);
}
public Money calculateDiscountAmount(Screening screening) {
for(DiscountCondition each : conditions) {
if(each.isSatisfiedBy(screening){
return getDiscountedFee(screening);
}
}
return screening.getMovieFee();
}
abstract protected Money getDiscountAmount(Screening screening);
}
위 DiscountPolicy의 로직에서 변하지 않는 부분은 할인 여부를 판단하는 로직이고 변하는
부분은 할인된 요금을 계산하는 방법이다. 추상화 과정을 통해 생략된 부분을 상속을 통해
구체화함으로써 할인 정책을 확장할 수 있다.
단순히 어떤 개념을 추상화했다고 해서 수정에 대해 닫혀 있는 설계를 만들 수 있는 것은 아니다.
OCP에서 폐쇄를 가능하게 하는 것은 의존성의 방향이다. 수정의 영향을 최소화하기 위해 모든
요소가 추상화에 의존해야 한다.
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee, DiscountPolicy discountPolicy) {
...
this.discountPolicy = discountPolicy;
}
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
Movie는 할인 정책을 추상화한 DiscountPolicy에 대해서만 의존한다. 할인 정책을
추가하기 위해 DiscountPolicy의 자식 클래스를 추가하더라도 Movie는 영향을 받지
않는다. 따라서 두 클래스는 수정에 대해 닫혀 있다.
정리하면, 추상화를 했다고 해서 OCP를 준수하는 형태가 되는 것은 아니다. 변하는 부분과
변하지 않는 부분을 이해하고 이를 추상화의 목적으로 삼아야 최적의 설계를 도출할 수 있다.
Movie가 오직 DiscountPolicy라는 추상화에 의존하기 위해서는 Movie 내부에서
AmountDiscountPolicy와 같은 구체 클래스의 인스턴스를 생성해서는 안 된다.
public class Movie {
...
private Discount discountPolicy;
public Movie(String title, Duration runningTime, Money fee){
...
this.discountPolicy = new AmountDiscountPolicy(...);
}
public Money calculateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening));
}
}
위 코드에서 할인 정책을 변경할 경우 다른 구체 클래스 인스턴스를 생성하도록 직접 코드를
수정해야 한다. 이는 기존 코드를 수정하게 만들어 OCP를 위반한다.
결합도가 높아질수록, 알아야 하는 지식이 많아질수록 OCP를 다르는 구조를 설계하기가
어려워진다. 특히 객체 생성에 대한 지식은 과도한 결합도를 초래하는 경향이 있다.
Movie 라는 동일한 클래스 내에서 객체 생성과 사용이라는 두 가지 이질적인 목적을
가진 코드가 공존하는 것이 문제이다.

최적의 설계를 원한다면 객체와 관련된 두 책임을 서로 다른 객체로 분리해야 한다. 한 마디로
객체에 대한 생성과 사용을 분리(separation use from creation)해야 한다.
소프트웨어 시스템은 (응용 프로그램 객체를 제작하고 의존성을 서로 "연결"하는) 시작
단계와 (시작 단계 이후에 이어지는) 실행 단계를 분리해야 한다.
사용으로부터 생성을 분리하는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로
옮기는 것이다. Movie에게 적용할 할인 정책을 알고 있는 것은 그 시점에 Movie와
협력할 클라이언트이기 때문에, 컨텍스트에 대한 결정권을 가지고 있는 클라이언트로 그에
관한 지식을 이동하는 것이 타당하다는 사실을 알 수 있다.
public class Client {
public Money getAvatarFee() {
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(...));
return avatar.getFee();
}
}

인스턴스를 생성하는 책임을 클라이언트에게 맡김으로써 구체적인 컨텍스트와 관련된 정보는
클라이언트로 옮기고 Movie는 추상화인 DiscountPolicy에만 의존하게 된다.
Movie를 사용하는 Client도 특정 컨텍스트에 묶이지 않기를 바란다고 가정해보자.
Client의 코드를 살펴보면 Movie의 인스턴스를 생성하는 동시에 getFee메시지도
함께 전송한다는 것을 알 수 있다. Client 역시 생성과 사용의 책임을 함께 지니고
있는 것이다. Movie 문제를 해결했던 방법과 동일한 방법으로 이 문제를 해결할 수 있는데
거기에 더하여 Client와 협력하는 클라이언트에게까지 객체 생성과 관련된 지식이
새어나가길 원하지 않는다고 생각해보자.
이 경우 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 Client는 이 객체를
사용하도록 만들 수 있다. 이처럼 객체 생성에 특화된 객체를 FACTORY라고 부른다.
public class Factory {
public Movie createAvatarMovie() {
return new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(...));
}
}
이제 Client는 Factory를 사용해서 생성된 Movie의 인스턴스를 반환받아 사용하기만
하면 된다.
public class Client {
private Factory factory;
public Client(Factory factory) {
this.factory = factory;
}
public Money getAvatarFee() {
Movie avatar = factory.createAvatarMovie();
return avatar.getFee();
}
}
FACTORY를 사용하면 Client 는 오직 사용과 관련된 책임만 지고 생성과 관련된 어떤 지식도
가지지 않을 수 있다.

도메인 모델은 INFORMATION EXPERT를 찾기 위해 참조할 수 있는 일차적인 재료다.
어떤 책임을 할당하고 싶다면 제일 먼저 도메인 모델 안의 개념 중에서 적절한 후보가
존재하는지 찾아봐야 한다.
하지만 FACTORY는 도메인 모델에 속하지 않는다. 이는 순수하게 기술적인 결정이다. 전체
결합도를 낮추고 제사용성을 높이기 위해 도메인 개념에 할당되어 있던 객체 생성 책임을
가공의 객체로 이동시킨 것이다.
크레이그 라만은 시스템을 객체로 분해하는 방식에 크게 표현적 분해(representational
decomposition)와 행위적 분해(behavioral decomposition)가 존재한다고 말한다.
표현적 분해는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을
분해하는 것으로 객체지향 설계를 위한 기본적인 접근법이다.
하지만 이런 접근법으로 객체에게 책임을 할당하는 방식만으로 부족한 경우가 존재한다.
모든 책임을 도메인 객체에게 할당하면 낮은 응집도, 높은 결합도와 같은 심각한 문제를
마주할 가능성이 높아진다. 이 경우 설계자가 편의를 위해 만들어낸 가공의 객체에게
문제를 해결해야 한다. 이처럼 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인
객체를 PURE FABRICATION(순수한 가공물)이라고 부른다.
어떤 행동을 추가하려 하는데 이 행동을 책임질 마땅한 도메인 개념이 존재하지 않는다면
PURE FABRICATION을 추가하고 이 객체에게 책임을 할당하라. 일반적으로 이 객체는
특정 행동을 표현한다. 따라서 보통 행위적 분해에 의해 생성된다.
이 측면에서 객체지향의 실세계의 모방이라는 말은 옳지 않다. 객체지향 어플리케이션은
도메인 개념 뿐 아니라 설계자들이 임의적으로 창조한 인공적 추상화들을 포함하고 있다.
이런 인공적으로 창조된 객체들이 더 많은 비중을 차지하는 것이 일반적이다.
먼저 도메인의 본질적 개념을 표현하는 추상화를 이용해 어플리케이션을 구축하기 시작하라.
만약 도메인 개념이 만족스럽지 못하다면 인공적인 객체를 창조하라.
PURE FABRICATION 패턴
INFORMATION EXPERT 패턴에 따라 책임을 할당한 결과가 바람직하지 않을 경우 대안으로
사용되는 패턴이다. 응집도와 결합도를 개선할 수 있다면 가공의 객체를 추가해서 책임을
옮기는 것을 고려하라.
FACTORY는 객체의 생성 책임을 할당할 만한 도메인 객체가 존재하지 않을 때 선택할 수 있는
PURE FABRICATION이다.
생성과 사용을 분리하면 Movie에는 오로지 인스턴스를 사용하는 책임만 남는다. 이는 외부의
다른 객체가 Movie에게 생성된 인스턴스를 전달해야 한다는 것을 의미한다. 이처럼 외부의
독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법을
의존성 주입이라고 부른다. 의존성을 해결하는 방법은 다음 3가지가 존재한다.
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(...));
위 코드와 같이 생성자에서 컴파일타임 의존성을 런타임 의존성을 대체하는 경우를 생성자 주입이라
부른다.
avatar.setDiscountPolicy(new AmountDiscountPolicy(...));
위와 같은 setter 주입의 장점은 언제라도 의존 대상을 교체할 수 있다는 것이다. 단점은 객체가
올바로 생성되기 위해 어떤 의존성이 필수적인지를 명시적으로 표현할 수 없다는 것이다.
메서드 주입은 메서드 호출 주입이라고도 부르며 메서드가 의존성을 필요로 하는 유일한
경우에 사용할 수 있다. 주입된 의존성이 한두 개의 메서드에서만 사용된다면 각 메서드의
인자로 전달하는 것이 나은 방법일 수 있다.
avatar.calculateDiscountAmount(screening, new AmountDiscountPolicy(...));
의존성 주입 외에 의존성을 해결할 수 있는 방법 중 하나는 SERVICE LOCATOR 패턴이다.
이는 의존성을 해결할 객체들을 보관하는 일종의 보관소이며, 객체가 직접 의존성을 해결해줄
것을 요청한다.
public class Movie {
private String title;
private Duration runningTime;
private Money fee;
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime, Money fee) {
this.title = title;
this.runningTime = runningTime;
this.fee = fee;
this.discountPolicy = ServiceLocator.discountPolicy();
}
}
public class ServiceLocator {
private static ServiceLocator soleInstance = new ServiceLocator();
private DiscountPolicy discountPolicy;
public static DiscountPolicy discountPolicy() {
return soleInstance.discountPolicy;
}
public static void provide(DiscountPolicy discountPolicy) {
soleInstance.discountPolicy = discountPolicy;
}
private ServiceLocator() {
}
}
Movie의 인스턴스가 AmountDiscountPolicy의 인스턴스에 의존하기를 원한다면 다음과
같이 ServiceLocator에 인스턴스를 등록하고 Movie를 생성하면 된다.
ServiceLocator.provide(new AmountDiscountPolicy(...));
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000));
이런 형태로 보면 SERVICE LOCATOR패턴은 의존성을 가장 쉽고 간단히 해결하는 것으로
보인다. 하지만 의존성을 감춘다는 큰 단점을 지니고 있어 잘 사용되지 않는다. Movie가
DiscountPolicy에 의존하고 있지만, Movie의 퍼블릭 인터페이스 어디에도 이 의존성에
대한 정보가 표시돼 있지 않다.
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000));
위 코드의 경우 Movie인스턴스 생성에 필요한 모두 인자를 전달한 것으로 보이나, 코드를
실행해보면 discountPolicy가 null이기 때문에 NPE가 발생한다.
위 예제를 통해 의존성을 구현 내부로 감출 경우 관련 문제가 컴파일타임이 아닌 런타임에
발견된다는 사실을 알 수 있다. 숨겨진 의존성은 문제 발견 시점을 런타임으로 미루게 된다.
한편, 의존성을 숨기는 코드는 단위 테스트 코드를 작성할 때도 ServiceLocator의 상태를
전 단위 테스트 케이스에서 공유해야 하는 상황을 초래하기 때문에, 각 단위 테스트는 서로
고립되어야 한다는 기본 원칙을 위반하게 한다.
모든 문제의 원인은 숨겨진 의존성이 캡슐화를 위반했기 때문이다. 클래스의 퍼블릭
인터페이스만으로 사용 방법을 이해할 수 있는 코드가 캡슐화 관점에서 훌륭한 코드다.
클래스 사용법을 익히기 위해 구현 내부를 샅샅이 뒤져야 한다면 그 클래스의 캡슐화는
무너진 것이다.
핵심은 명시적인 의존성이 숨겨진 의존성보다 좋다는 것이다. 가급적 의존성을 객체의
퍼블릭 인터페이스에 노출하라.
깊은 호출 계층에 걸쳐 동일 객체를 계속해서 전달해야 하는 유별난 경우에만 SERVICE
LOCATOR 패턴을 사용을 고려하라.
public class Movie {
private AmountDiscountPolicy discountPolicy;
}
위와 같은 설계가 취약한 이유는 요금을 계산하는 상위 정책이 요금을 계산하는 데 필요한
구체적인 방법에 의존하기 때문이다.
객체 사이의 협력이 필요할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다.
Movie와 AmountDiscountPolicy사이의 협력이 가지는 본질은 영화의 가격을 계산하는
것이다. 어떻게 할인 금액을 계산할 것인지는 협력의 본질이 아니다.
상위 수준의 클래스인 Movie가 하위 수준의 AmountDiscountPolicy에 의존한다면
하위 수준의 변경에 영향을 받게 된다. 의존성은 상위에서 하위가 아닌 역뱡항으로 흘러야
한다. 또한 우리가 재사용하려는 대상은 상위 수준의 클래스이기 때문에 위같은 형태에서
재사용을 위해 하위 수준의 클래스도 동반되어 재사용이 어려워진다.
해결사는 추상화다. Movie와 DiscountPolicy 모두 추상화에 의존하도록
수정하면 하위 수준의 변경에 상위 수준이 영향을 받지 않으며, 상위 수준을 재사용할 시
하위 수준에 얽메이지 않고 다양한 컨텍스트에서 사용이 가능하다.

중요한 것은 추상화의 의존하라는 것이다. 구체 클래스는 의존성의 시작점이 되어야지
목적지가 되어서는 안 된다.
- 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다.
둘 모두 추상화에 의존해야 한다.- 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.
이를 의존성 역전 원칙(Dependency Inversion Principle, DIP)라 부른다.
'역전'은 DIP를 따르는 설계에서 의존성의 방향이 전통적인 절차형 프로그래밍과
반대 방향으로 나타나는 것을 의미한다.
역전은 의존성의 방향뿐만 아니라 인터페이스의 소유권에도 적용된다. OOP 언어에서
어떤 요소의 소유권을 결정하는 것은 모듈이며, 자바는 패키지를 통해 이를 구현한다.

위 같은 구조에서 Movie가 정상적으로 컴파일되기 위해서는 DiscountPolicy가
필요하다. 문제는 DiscountPolicy 클래스에 의존하기 위해서 반드시 같은 패키지에
포함된 구체 클래스들도 함께 존재해야 한다는 것이다.
DiscountPolicy가 포함된 패키지 내의 어떤 클래스가 수정되더라도 패키지 전체가
재배포돼야 한다. 이로 인해 이 패키지에 의존하는 Movie클래스가 포함된 패키지 역시
재컴파일돼야 한다. 불필요한 클래스들을 같은 패키지에 두는 것은 전체 빌드 시간을
가파르게 상승시킨다.

의존하는 추상화를 클라이언트가 속한 패키지에 포함시켜야 한다. 그리고 함께 재사용될
필요가 없는 클래스들을 별도의 독립적 패키지에 모아야 한다. 이 기법을 가리켜
SEPARATED INTERFACE 패턴이라 칭한다.
DIP에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스
소유권 역시 역전시켜야 한다. 최적의 OOP 애플리케이션에서는 인터페이스의 소유권을
서버가 아닌 클라이언트에 위치시킨다.
유연하고 재사용 가능한 설계란 런타임 의존성과 컴파일타임 의존성의 차이를 인식하고
동일한 컴파일타임 의존성으로부터 다양한 런타임 의존성을 만들 수 있는 코드 구조를
가지는 설계를 의미한다. 하지만 유연한 설계는 복잡한 설계라는 의미를 내포하고 있으며
단순함과 명확함에서 멀어질 가능성이 높다.
변경은 예상이 아니라 현실이어야 한다. 아직 일어나지 않은 변경에 대한 불안감은
막연하게 복잡한 설계를 낳는다. 설계가 유연할수록 클래스 구조와 객체 구조 사이의
거리는 점점 멀어지며 불필요한 유연성은 불필요한 복잡성을 초래한다. 복잡성에 대한
걱정보다 유연하고 재사용 가능한 설계의 필요성이 더 크다면 코드의 구조와 실행 구조를
다르게 만들어라.
설계를 유연하게 만들기 위해서는 결국 협력에 참여하는 객체가 다른 객체에게 어떤
메시지를 전송하는지가 중요하다. 다양한 컨텍스트에서 협력을 재사용할 필요가 없다면
설계를 유연하게 만들 당위성도 사라진다. 객체들이 메시지 전송자의 관점에서 동일한
책임을 수행하는지 여부를 판단할 수 없다면 공통의 추상화를 도출할 수 없다.
초보자가 자주 저지르는 실수 중 하나는 객체의 역할과 책임이 자리 잡기 전에 너무
성급하게 객체 생성에 집중하는 것이다. 이는 객체 생성과 관련된 불필요한 세부사항에
객체를 결합시킨다. 책임을 할당하고 협력의 균형을 맞추는 것이 객체 생성보다 최우선이다.
불필요한 SINGLETON 패턴은 객체 생성에 대해 너무 이른 시기에 고민하고 결정할 때
도입되는 경향이 있다.
핵심은 객체를 생성하는 방법에 대한 모든 결정은 모든 책임이 자리를 잡은 후 가장
마지막 시점에 내리는 것이 적절하다는 것이다.