설계원칙을 통해 의존성 관리 기법을 정리해보자.
01 개방-폐쇄 원칙(Open-Closed Principle, OCP)
- 소프트웨어 개체(클래스, 모듈, 함수 등등)는 확장에 대해 열려있어야 하고, 수정에 대해 닫혀있어야한다
- 확장에 대해 열려있다 : 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 '동작'을 추가해서 기능을 확장할 수 있다
- 수정에 대해 닫혀있다 : 기존의 '코드'를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다.
컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라
런타임 의존성 - 실행시에 협력에 참여하는 객체들 사이의 관계
컴파일타임 의존성 - 코드에서 드러나는 클래스들 사이의 관계
[Movie] -> [DiscountPolicy]
| |
[Percent DiscounPolicy] [Amount DiscounPolicy]
컴파일타임 의존성
[:Movie] - [:Amount DiscounPolicy]
[:Movie] - [:Percent DiscounPolicy]
런타임 의존성
새로운 클래스 [Overlapped DiscountPolicy]를 추가하는 것만으로 새로운 할인정책을 확장할 수 있다.
추상화가 핵심이다
추상화에 의존하는 것
핵심적인 부분만 남기고 불필요한 부분은 생략함으로써 복잡성을 극복하는 기법
생략되지 않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물.
생략된 부분을 문맥에 적합한 내용으로 채워넣음으로써 각 문맥에 적합하게 기능을 구체화하고 확장
변하지 않는 부분 - 할인여부를 판단하는 로직
변하는 부분 - 할인된 요금을 계산하는 방법
변하는 부분을 고정하고 변하지 않는 부분을 생략
public abstract class DiscountPolicy {
private List<DiscountCondition> conditions = new ArrayList();
public DiacountPolicy(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);
}
Movie는 할인정책을 추상화한 DiscountPolicy에만 의존
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(dicountPolicy.calculateDiscountAmount(screening));
}
}
02 생성 사용 분리
동일한 클래스 안에서 객체 생성과 사용이라는 두가지 이질적인 목적을 가진 코드가 공존하는 것이 문제
객체와 관련된 두 가지 책임을 서로 다른 객체로 분리해야 한다.
객체를 생성할 책임을 클라이언트에 옮기자.
클라이언트로 컨텍스트에 대한 지식을 옮김으로서 특정 클라이언트에 결합되지 않고 독립적일 수 있다.
FACTORY 추가하기
객체 생성과 관련된 지식이 Client와 협력하는 클라이언트에게 까지 새어나가기를 원하지 않는다고 가정하자.
이 경우 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 Client는 이 객체를 사용하도록 만들수 있다.
생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY 라고 부름.
public class Factory {
public Movie createAvatarMovie() {
return new Movie("아바타",
Duration.ofMinutes(120),
Monew.wons(10000),
new AmountDiscountPolicy(...));
}
}
public class Client {
private Factory factory;
public Client(Factory factory) {
this.factory = factory;
}
public Money getAvararFee() {
Movie avatar = factory.createAvatarMovie();
return avatar.getFee();
}
}
Client는 오직 사용과 관련된 책임만 지고 생성과 관련된 어떤 지식도 가지지 않음.
순수한 가공물에 책임 할당하기
시스템을 객체로 분해하는 두가지 방법
- 표현적 분해(representational decomposition)
: 도메인에 존재하는 사물 또는 개념을 표현하는 객체들을 이용해 시스템을 분해하는 것.
- 행위적 분해(behavioral decomposition)
: 어떤 행동을 추가하려고 하는데 이것을 책임질 마땅한 도메인 개념이 존재하지 않는다면 pure fabricatioin을 추가하고 이 객체에 책임을 할당하자. 순수한 가공물(PURE FABRICATION) :책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체
객체지향 애플리케이션 내에서 인공적으로 창조한 객체들이 도메인 개념을 반영하는 객체들보다
오히려 더 많은 비중을 차지하는것이 일반적
대부분의 디자인 패턴은 pure fabrication을 포함한다.
03 의존성 주입 (Dependency Injection)
사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법
외부에서 의존성의 대상을 해결한 후 이를 사용하는 객체 쪽으로 주입함.
chapter 08 에서 이야기한 의존성 해결 방법과 관련이 깊음.
Movie avatar = new Movie("아바타", Duration.ofMinutes(120), Money.wons(10000), new AmountDiscountPolicy(...));
- 생성자 주입(constructor injection) : 객체를 생성하는 시점에 생성자를 통한 의존성 해결. 생성자 주입을 통해 설정된 인스턴스는 객체의 생명주기 전체에 걸쳐 관계를 유지.
avatar.setDiscountPolicy(new AmountDiscountPolicy(...));
- setter 주입(setter injection) : 객체 생성 후 setter 메서드를 통한 의존성 해결. 설정된 인스턴스는 언제라도 의존 대상을 교체할 수 있음. setter 메서드는 객체가 생성된 후에 호출 해야 하기 때문에 메서드 호출을 누락한다면 객체는 비정상적인 상태로 생성됨.
avatar.calculateDiscountAmount(screening, new AmountDiscountPolicy(...));
- 메서드 주입(method call injection) : 메서드 실행 시 인자를 이용한 의존성 해결. 주입된 의존성이 한 두개의 메서드에서만 사용된다면 각 메서드의 인자로 전달하는것이 효율적.
숨겨진 의존성을 나쁘다
의존성 주입 외에 의존성을 해결할 수 있는 방법 중 가장 널리 사용되는 대표적인 방법 - SERVICE LOCATOR
객체들을 보관하는 일종의 저장소
객체가 직접 SERVICE LOCATOR에게 의존성을 해결해줄 것을 요청
Movie는 직접 ServiceLocator 의 메서드를 호출해서 DiscountPolicy에 대한 의존성을 해결
public class Movie {
...
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 생성
ServiceLocator.provide(new AmountDiscountPolicy(...));
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Moneym.wons(10000));
단점은 의존성을 감춘다는 것.
Movie 는 DiscountPolicy에 의존하고 있지만, 이 의존성에 대한 정보는 표시되 있지 않으므로.
의존성을 구현 내부로 감출 경우 의존성과 관련된 문제가 컴파일타임이 아닌 런타임에 가서야 발견됨.
이것은 단위 테스트로 어렵게 함. ServiceLocator는 상태를 공유하게 되는데,
각 단위 테스트는 서로 고립돼야 한다는 단위테스트의 기본 원칙을 위배한다.
숨겨진 의존성은 캡슐화를 위반한다. 명시적인 의존성이 숨겨진 의존성보다 좋다.
의존성 주입을 지원하는 프레임워크를 사용하지 못하는 경우나 깊은 호출계층에 걸쳐 동일한 객체를 계속해서 전달해야 하는 어려운 경우에는 어쩔수 없이 SERVICE LOCATOR패턴을 사용하는 것을 고려하라.
접근해야 할 객체가 있다면 전역 매커니즘 대신, 필요한 객체를 인수로 넘겨줄 수는 없는지부터 생각해보자. 이 방법은 굉장히 쉬운데다 겹합을 명확하게 보여줄 수 있다. 대부분은 이렇게만 해도 충분하다.
04 의존성 역전 원칙
추상화와 의존성 역전
public class Movie {
private AmountDiscountPolicy discountPolicy;
}
Movie가 변경에 취약한 이유 : 요금을 계산하는 상위 정책이 요금을 계산하는 데 필요한 구체적인 방법에 의존하기 때문.
상위수준 클래스인 Movie가 하위수준 클래스에 의존함.
하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것이기 때문
협력의 본질을 담고 있는것음 상위 수준의 정책이다.
의존성의 방향이 잘못됐다.
상위 수준의 클래스를 재사용 할 때 하위 수준의 클래스도 필요하기 때문에 재사용하기 어려워진다.
-> 해결방법은 추상화
하위수준 클래스에 얽히지 않고 다양한 컨텍스트에서 재사용 가능.
의존성 역전 원칙(Dependency Inversion Principle, DIP)
- 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다
- 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항은 추상화에 의존해야 한다.
잘 설계된 객체지향 프로그램의 의존성 구조는 전통적인 절차적 방법에 의해 일반적으로 만들어진 의존성 구조에 대해 '역전'된 것이다.
의존성 역전 원칙과 패키지
역전은 의존성의 방향 뿐만 아니라 인터페이스의 소유권에도 적용
어떤 구성요소의 소유권을 결정하는 것은 모듈
자바는 패키지, C#, C++은 네임스페이스를 이용해 모듈을 구현
Movie를 정상적으로 컴파일하기 위해서는 DiscountPolicy 클래스가 필요하다.
코드의 컴파일이 성공하기 위해 함께 존재해야 하는 코드를 정의하는 것이 바로 컴파일 의존성이다.
불필요한 클래스들을 같은 패키지에 두는 것은 전체적인 빌드 시간을 가파르게 상승시킨다.
따라서 추상화를 별도의 독립적인 패키지가 아니라 클라이언트가 속한 패키지에 포함시켜야 한다. 그리고 재사용될 필요가 없는 클래스들은 별도의 독립적인 패키지에 모아야 한다. - SEPARATED INTERFACE 패턴
의존성 역전 원칙에 따랄 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권 역시 역전시켜야 한다.
05 유연성에 대한 조언
유연한 설계는 유연성이 필요할 때만 옳다
유연하고 재사용 가능한 설계란?
런타임 의존성과 컴파일타임 의존성의 차이를 인식하고
동일한 컴파일 타임 의존성으로부터 다양한 의존성을 만들 수 있는 코드 구조를 가지는 설계를 의미.
유연성은 복잡성을 수반한다.
절차적인 프로그래밍 방식으로 작성된 코드는 코드에 표현된 정적인 구조가 곧 실행 시점의 동적인 구조를 의미
객체지향 코드에서 클래스의 구조는 발생 가능한 모든 객체 구조를 담는 틀일 뿐이므로, 객체를 생성하거나 변경하는 부분을 모두 살펴봐야함.
하지만 불필요한 유연성은 불필요한 복잡성을 만든다.
단순하고 명확한 설계보다 유연하고 재사용가능한 설계의 필요성이 클때에 실행구조를 다르게 만드는것이 필요할 것이다.
협력과 책임이 중요하다
협력에 참여하는 객체가 전송하는 메시지가 중요하다.
설계를 유연하게 만들기 위해서는 먼저 역할, 책임, 협력에 초점을 맞춰야 함.
다양한 컨텍스트에서 협력을 재사용할 필요가 없다면 설계를 유연하게 만들 당위성도 없음.
객체들이 메시지 전송자의 관점에서 동일한 책임을 수행하는지 여부를 판단해 공통의 추상화를 도출해야함.
객체를 생성하는 방법은 책임할당의 마지막 단계로 미뤄야함.
역할, 책임, 협력에 먼저 집중하자.
'IT Book Summary > Object: 객체지향설계' 카테고리의 다른 글
Chapter 11 합성과 유연한 설계 (0) | 2020.01.01 |
---|---|
Chapter 10 상속과 코드 재사용 (0) | 2019.12.25 |
Chapter 08 의존성 관리하기 (0) | 2019.12.11 |
Chapter 07 객체분해 (0) | 2019.12.04 |
Chapter 06 메시지와 인터페이스 (0) | 2019.11.25 |