오브젝트 Ch.9 — 유연한 설계
개방-폐쇄 원칙, FACTORY와 PURE FABRICATION, 의존성 주입과 SERVICE LOCATOR, 의존성 역전 원칙을 정리한다.
시리즈 · BackEnd 서적15 / 17
- SQL 레벨업 Ch.1 — DBMS 아키텍처
- SQL 레벨업 Ch.3 — SQL의 조건 분기
- SQL 레벨업 Ch.4~5 — 집약과 자르기, 반복문
- SQL 레벨업 Ch.6 — 결합
- SQL 레벨업 Ch.7 — 서브쿼리
- SQL 레벨업 Ch.8 — SQL의 순서
- SQL 레벨업 Ch.9 — 갱신과 데이터 모델
- SQL 레벨업 Ch.10 — 인덱스 사용
- 오브젝트 Ch.1 — 객체, 설계
- 오브젝트 Ch.2 — 객체지향 프로그래밍
- 오브젝트 Ch.3~4 — 역할, 책임, 협력 / 설계 품질과 트레이드오프
- 오브젝트 Ch.5 — 책임 할당하기
- 오브젝트 Ch.6 — 메시지와 인터페이스
- 오브젝트 Ch.8 — 의존성 관리하기
- 오브젝트 Ch.9 — 유연한 설계
- 오브젝트 Ch.10, 12 — 상속과 코드 재사용, 다형성
- 오브젝트 Ch.13 — 서브클래싱과 서브타이핑
8장에서 다룬 의존성 관리 기법들에 이름을 붙이고 원칙으로 정리하는 장이다.
개방-폐쇄 원칙 (OCP)
소프트웨어 개체는 확장에 대해 열려 있어야 하고, 수정에 대해서는 닫혀 있어야 한다.
- 확장에 열려 있다: 요구사항이 변경될 때 새로운 ’동작’을 추가해 기능을 확장할 수 있다.
- 수정에 닫혀 있다: 기존의 ’코드’를 수정하지 않고도 동작을 추가하거나 변경할 수 있다.
의존성 관점에서 보면, 컴파일타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조다.
핵심은 추상화다. 올바른 추상화를 설계하고 추상화에만 의존하도록 관계를 제한하면 설계를 유연하게 확장할 수 있다. 다만 추상화를 했다고 모든 수정에 대해 닫히는 것은 아니다. 변경되지 않을 부분을 신중하게 결정하고 올바른 추상화를 선택했을 때에만 닫혀 있을 수 있다.
생성과 사용의 분리: FACTORY
객체 생성 책임만 전담하는 별도의 객체를 두고 클라이언트는 이 객체를 사용하게 만든다. 이렇게 생성에 특화된 객체를 FACTORY라고 부른다. 1장 정리에서 직접 만들어 본 TheaterCeo가 바로 이 역할이었다.
public class Factory {
public Movie createAvatarMovie() {
return new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(100000),
new AmountDiscountPolicy(...));
}
}
public class Client {
private Factory factory;
public Client(Factory factory) {
this.factory = factory;
}
public Money getAvatarFee() {
Movie avatar = factory.createAvatarMovie();
return avatar.getFee();
}
}
클라이언트는 Factory가 만들어 준 Movie 인스턴스를 사용하기만 하면 된다. 그런데 Factory는 도메인에 존재하는 개념이 아니다. 책임은 INFORMATION EXPERT에게 할당하는 것이 기본인데, Factory는 순전히 설계상의 필요로 만들어진 객체다.
PURE FABRICATION
크레이그 라만은 시스템을 객체로 분해하는 방식을 둘로 나눈다.
- 표현적 분해(representational decomposition): 도메인에 존재하는 사물이나 개념을 표현하는 객체들로 시스템을 분해한다. 객체지향 설계의 가장 기본적인 접근법이다.
- 행위적 분해(behavioral decomposition)
모든 책임을 도메인 객체에만 할당하면 낮은 응집도, 높은 결합도, 재사용성 저하 같은 문제에 부딪힐 가능성이 높다. 이럴 때는 설계자가 편의를 위해 임의로 만들어 낸 가공의 객체에게 책임을 할당한다. 책임을 할당하기 위해 창조되는, 도메인과 무관한 인공적인 객체를 PURE FABRICATION(순수한 가공물)이라고 한다.
어떤 행동을 책임질 마땅한 도메인 개념이 없다면 PURE FABRICATION을 추가하고 그 객체에게 책임을 할당한다. 보통 특정한 행동을 표현하므로 표현적 분해보다는 행위적 분해로 만들어진다. FACTORY가 그 예다.
의존성 주입 (Dependency Injection)
사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법이다.
- 생성자 주입(constructor injection)
- setter 주입(setter injection)
- 메서드 주입(method injection)
생성자 주입으로 설정된 인스턴스는 객체의 생명 주기 전체에 걸쳐 관계를 유지한다. 반면 setter 주입은 런타임에 언제라도 의존 대상을 교체할 수 있다. 대신 다음과 같은 단점이 있다.
- 객체가 올바로 생성되기 위해 어떤 의존성이 필수인지 명시적으로 표현할 수 없다.
- setter 호출을 누락하면 객체가 비정상적인 상태로 남는다.
SERVICE LOCATOR
의존성을 외부에서 전달받는 대신, 객체가 ServiceLocator의 메서드를 직접 호출해 필요한 의존성을 얻는다.
public class Movie {
// ...
private DiscountPolicy discountPolicy;
public Movie(String title, Duration runningTime) {
this.title = title;
this.runningTime = runningTime;
// static 메서드로 직접 요청한다
this.discountPolicy = ServiceLocator.discountPolicy();
}
}
가장 큰 단점은 의존성을 감춘다는 것이다. 생성자 시그니처만 봐서는 Movie가 DiscountPolicy를 필요로 한다는 사실을 알 수 없고, 문제를 발견하는 시점이 코드 작성 시점이 아닌 실행 시점으로 미뤄진다.
의존성 역전 원칙 (DIP)
public class Movie {
private AmountDiscountPolicy discountPolicy;
}
상위 수준 클래스인 Movie가 하위 수준 클래스인 AmountDiscountPolicy에 의존하고 있다.
- Movie를 재사용하려면 AmountDiscountPolicy도 함께 가져가야 하므로 재사용이 어렵다.
- 상위 수준의 변경으로 하위 수준이 변경되는 것은 납득할 수 있지만, 하위 수준의 변경으로 상위 수준이 변경돼서는 곤란하다.
이 경우에도 해결책은 추상화다. Movie가 AmountDiscountPolicy가 아닌 추상화인 DiscountPolicy에 의존하게 만든다.
- 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안 된다. 둘 모두 추상화에 의존해야 한다.
- 추상화는 구체적인 사항에 의존해서는 안 된다. 구체적인 사항이 추상화에 의존해야 한다.
마무리
중요한 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞추는 것이 객체 생성에 관한 책임을 할당하는 것보다 우선이다. 객체를 생성하는 방법에 대한 결정은 모든 책임이 자리를 잡은 뒤 가장 마지막에 내리는 것이 적절하다.
역할, 책임, 협력의 모습이 선명하게 그려지지 않는다면 의존성을 관리하는 데 들이는 모든 노력이 물거품이 될 수 있다.