# 09장 유연한 설계
조영호 저 "오브젝트: 코드로 이해하는 객체지향 설계, 2019"를 읽고 정리한 내용입니다.
1 개방-폐쇄 원칙 OCP
소프트웨어 개체는 (클래스, 모듈, 함수 등) 확장에 열려 있어야 하고, 수정에 대해서느 닫혀있어야 한다.
요구사항 변경에 맞게 새로운 동작을 추가해서 기능을 확장할 수 있다.
기존 코드를 수정하지 않고도애플리케이션 동작을 추가 변경할 수 있다.
어떻게 코드를 수정하지 않고도 새로운 동작을 추가할 수 있단 말인가?
컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라
DiscountPolicy를 보면 Amount와 Percent를 바꿔 낄 수 있었다. 이럼으로써 새로운 할인정책을 추가해서 기능을 확장할 수 있게 된다. ( = 확장에 대해 열려 있다 ) 그리고 기존 코드를 수정할 필요 없이 새로운 클래스를 추가하는 것만으로 새로운 할인정책을 확장할 수 있다. ( = 수정에 대해 닫혀 있다 )
추상화가 핵심이다
추상화란 핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법이다. 추상화 과정을 거치면 문맥이 바뀌더라도 변하지 않는 부분만 남게 되고 문맥에 따라 변하는 부분은 생략된다. 생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장할 수 있다.
공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 다시 말해 수정할 필요가 없어야 한다.
코드로 확인해보자
변하지 않는 부분은 할인여부를 판단하는 로직 calculateDiscountAmount, 변하는 부분은 할인된 요금을 계산하는 방법 getDiscountAmount
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 getDiscountAmount(screening);
}
}
return Money.getMovieFee();
}
abstract protected Money getDiscountAmount(Screening Screening);
}OCP에서 폐쇄를 가능하게 하는 것은 의존성의 방향이다. Movie는 할인 정책을 추상화한 DiscountPolicy에 대해서만 의존한다. DiscountPolicy는 변하지 않는 추상화이다. Movie는 안정화된 추상화 DiscountPolicy에 의존하기 때문에 정책을 추가해도 영향을 받지 않는다. 따라서 Movie는 DiscountPolicy 수정에 대해 닫혀있다.
올바른 추상화를 설계하고 추상화에 대해서만 의존하도록 관계를 제한함으로써 설계를 유연하게 확장할 수 잇다.
이 모든 과정은 변하는 것과 변하지 않는 것을 파악하였기 때문에 가능한 설계이다.
2 생성 사용 분리
Movie 내부에서 AmountDiscountPolicy 같은 구체 클래스의 인스턴스를 생성해서는 안된다. OCP 위반.
객체생성은 필수지만, 부적절한 곳에서 생성한게 문제이다.
Movie 코드안에 calculateMovieFee 메서드에서 DiscountPolicy 객체에게 메시지를 전송한다. 메시지를 전송하지 않고 객체만 생성했다면 아무런 문제가 없었을 것이다. 또는 메시지만 전송했다면 괜찮았을 것이다. 동일한 클래스 안에서 객체생성과 사용이라는 두가지 이질적인 목적을 가진 코드가 공존한다는 것이 문제이다. (무슨말?).

유연한 설계를 원한다면 두가지 책임을 서로 다른 객체로 분리해야 한다. 즉, 생성과 사용을 분리해야 한다. (seperating use from creation)
사용과 생성 분리를 하기 위해 보편적으로 객체를 생성할 책임을 클라이언트에 옮기는 것이다.

하지만 생성 책임을 Client로 옮기고 클라이언트도 사용과 생성을 함께 가져가게 되었다. 이렇게 옮긴 배경에는 영화는 특정 컨택스트에 묶이면 안되지만 클라이언트는 묶여도 상관 없다는 전제가 깔려있다.
객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가해 (팩토리) 유연하게 만들어 나갈 수 있다.
FACTORY 추가하기
Factory: 생성과 사용 분리하기 위해 객체생성에 특화된 객체

순수한 가공물에게 책임 할당하기
5장에서 책임 할당 원칙을 패턴의 형태로 기술한 GRASP 패턴에 관해 살펴봤다. (정보전문가 개념) 어떤 책임을 할당하고 싶다면 제일 먼저 도메인 모델 안의 개념중에서 적절한 후보가 존재하는지 찾아봐야 한다.
사실 팩토리는 도메인 모델에 속하지 않는다. 이 결정은 순수하게 기술적인 결정이었다. 결합도를 낮추고 재사용성을 높이기 위해 도메인 개념에게 할당돼 있던 생성책임을 도메인 개념과는 상관이 없는 가공의 객체로 이동시킨 것이다.
시스템을 객체로 분해하는 방법
표현적 분해 representational decomposition
행위적 분해 behavior decomposition
표현적 분해는 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하며, 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는 것을 목적으로 한다. 객체지향을 위한 기본적인 접근법.
행위적 분해는 어떤 행위를 추가하려고 하는데 마땅한 도메인 개념이 없을때 순수한 인공물 PURE FABRICATION 가공 객체에게 책임을 할당한다.
모든 책임을 도메인 객체에게만 할당하면 여러 문제에 봉착해 도메인 개념을 초월하는 기계적인 개념(창조적인 인공 추상화)들이 필요할 수 있다. 예, 데이터베이스 접근을 위한 객체. 도메일 모델은 출발점이라는점을 명심한다.
이런 측면에서 객체지향이 실세계 모방이란 말은 옳지 않다. 대부분의 애플리케이션에서는 실제 도메인에서 발견할 수 없는 순수 인공물로 가득 차 있다. 따라서 도메인 개념을 표현하는 객체와 순수하게 창조된 가공의 객체들이 모여 자신의 역할과 책임을 다하고 조화롭게 협력하는 애플리케이션 설계하는 것이 목표여야 한다.
순수 인공물 pure fabrication
퓨어 패브리케이션은 정보전문가 패턴에 따라 책을 할당한 결과가 바람직하지 않을 경우 대아으로 사용된다. 즉, 전 장들에서 다루었던 것처럼 도메인개념에 적절히 책임을 분배했는데도 결합도, 응집도 관련 문제가 생긴다면은 순수 인공물을 추가한다.
3 의존성 주입
의존성 주입: 외부의 독립 객체가 인스턴스를 생성한 후 이를 전달해 의존성을 해결하는 기법.
의존성 해결은 컴파일타임 의존성과 런타임 의존성의 차이점을 해소하기 위한 다양한 매커니즘을 포괄한다.
의존성을 해결하는 세가지 방법
생성자 주입
setter 주입
메서드 주입
숨겨진 의존성은 나쁘다
의존성 주입 외에도 의존성을 해결할 수 있는 다양한 방법이 존재한다. SERVICE LOCATOR 패턴이다. 의존성 해결할 객체들을 보관하는 일종의 저장소이다.
서비스 로케이터 패턴의 가장 큰 단점은 의존성을 숨긴다. Movie 퍼블릭 인터페이스 어디에도 DiscountPolicy에 의존한다는 정보가 없다.
따라서 avatar.calculateMovieFee(screening); 를 실행 했을때, discountPolicy가 null 이면 실행시점 에러가 발생한다. 문제점을 발견할 수 있는 시점을 코드 작성 시점이 아니라 실행 시점으로 미루기에 숨겨진 의존성을 찾고 디버깅하는게 어려울 수도 있다.
일반적으로 단위 테스트 프레임워크는 테스트 케이스 단위로 테스트에 사용될 객체들을 새로 생성하는 기능을 제공한다. 하지만 ServiceLocator는 내부적으로 정적변수를 사용해 객체들을 관리하기에 모든 단위 테스트 케이스에 걸쳐 ServiceLocator의 상태를 공유하게 된다. 이것은 각 단위테스트는 서로 고립돼야 한다는 기본 원칙을 위반한다.
아래는 이해가 안되서 책에 있는 것을 그대로 쓴는데 추후 요약하자..
단위 테스트가 서로 간섭 없이 실행 되기 위해서는 Movie를 테스트 하는 모든 단위 테스트 케이스에서 무비를 생성하기 전에 서비스로케이터에 필요한 DicountPolicy 인스턴스를 추가하고 끝날때 마다 추가된 인스턴스를 제거해야한다. 문제의 원인은 숨겨진 의존성이 캡슐화를 위반했기 때문이다. 단순히 인스턴스 변수의 가시성을 숨겼다고 (private) 해서 캡슐화가 지켜지는 것은 아니다.
이야기의 핵심은 의존성 주입이 SERVICE LOCATOR 보다 좋다가 아니라 명시적인 의존석이 숨겨진 의존성 보다 좋다는 것이다.
SERVICE LOCATOR는 여러 계층으로 호출되어 객체를 계속 전달하는 상황이 꼴보기 싫을 때나, 의존성 주입이 안되는 프레임워크 사용시 사용될 수 있다.
4 의존성 역전 원칙
추상화와 의존성 역전
의존성 역전 원칙 (Dependency Inversion Principle, DIP)
상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.
추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.
의존성 역전 원칙과 패키지

Movie를 다양한 컨텍스트에서 재사용하기 위해서는 불필요한 클래스들이 Movie와 함께 배포돼야만 한다. DiscountPolicy가 포함돼 있는 패키지 안에 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스가 포함돼 있다는 것이다. 이것은 DiscountPolicy 클래스에 의존하기 위해서는 반드시 같은 패키지에 포함된 AmountDiscountPolicy 클래스와 PercentDiscountPolicy 클래스도 함께 존재해야 한다는 것을 의미한다.
DiscountPolicy가 포함된 패키지 안의 어떤 클래스가 수정되더라도 패키지 전체가 재배포돼야 한다. 따라서 필요한 클래스들을 같은 패키지에 두는 것은 전체적인 빌드 시간을 가파르게 상승시킨다.
SEPARATED INTERFACE 패턴
추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다. 그리고 함께 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다.

잘 설계된 객체지향 애플리케이션에서는 그림 9.10과 같이 인터페이스의 소유권을 서버가 아닌 클라이언트에 위치시킨다.
전통적인 패러다임에서는 인터페이스가 하위 수준 모듈에 속했다면 객체지향 패러다임에서는 인터페이스가 상위 수준 모듈에 속한다.
5 유연성에 대한 조언
유연한 설계는 유연성이 필요할 때만 옳다
설계의 미덕은 단순함과 명확함으로부터 나온다. 설계가 유연할수록 클래스 구조와 객체 구조 사이의 거리는 점점 멀어진다. 따라서 유연함은 단순성과 명확성의 희생 위에서 자라난다.
절차적인 프로그래밍 방 식으로 작성된 코드는 코드에 표현된 정적인 구조가 곧 실행 시점의 동적인 구조를 의미한다. 객체지향 코드에서 클래스의 구조는 발생 가능한 모든 객체 구조를 담는 틀일 뿐이다. 특정 시점의 객체 구조를 파악하는 유일한 방법은 클래스를 사용하는 클라이언트 코드 내에서 객체를 생성하거나 변경하는 부분을 직접 살펴보는 것뿐이다.
협력과 책임이 중요하다
협력에 참여하는 객체가 다른 객체에게 어떤 메시지를 전송하는지가 중요하다.
Movie가 다양한 할인 정책과 협력할 수 있는 이유는 무엇인가? 모든 할인 정책이 Movie가 전송하는calculateDiscountAmount 메시지를 이해할 수 있기 때문이다.
핵심은 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 후 가장 마지막 시점에 내리는 것이 적절하다는 것이다.
Last updated