8장에서 설명한 기법들을 원칙이라는 관점에서 정리한다.
1. 개방-폐쇄 원칙
: 소프트웨어 개체(클래스, 모듈, 함수 등)는 '확장'에 대해 열려있어야 하고, '수정'에 대해서는 닫혀있어야 한다.
- '확장'에 대해 열려 있다 : 애플리케이션의 요구사항이 변경될 때 이 변경에 맞게 새로운 '동작'을 추가해서 애플리케이션의 기능을 확장할 수 있다.
- '수정'에 대해 닫혀 있다 : 기존의 '코드'를 수정하지 않고도 애플리케이션의 동작을 추가하거나 변경할 수 있다.
<컴파일 타임 의존성을 고정시키고 런타임 의존성을 변경하라>
- 컴파일 타임 의존성은 코드에서 드러나는 클래스들 사이의 관계, 런타임 의존성은 실행 시에 협력에 참여하는 객체들 사이의 관계다. 유연하고 재사용 가능한 설계에서 런타임 의존성과 컴파일 의존성은 서로 다른 구조를 가진다.
- 의존성 관점에서 개방-폐쇄 원칙을 따르는 설계란 컴파일 타임 의존성은 유지하면서 런타임 의존성의 가능성을 확장하고 수정할 수 있는 구조라고 할 수 있다.
<추상화가 핵심이다>
- 개방-폐쇄 원칙의 핵심은 추상화에 의존하는 것이다. 개방-폐쇄 원칙의 관점에서 생략되지 않고 남겨지는 부분은 다양한 상황에서의 공통점을 반영한 추상화의 결과물이다. 공통적인 부분은 문맥이 바뀌더라도 변하지 않아야 한다. 다시 말해서 수정할 필요가 없어야 한다. 따라서 추상화 부분은 수정에 닫혀 있다. 추상화를 통해 생략된 부분은 확장의 여지를 남긴다. 이것이 추상화가 개방-폐쇄 원칙을 가능하게 만드는 이유다.
2. 생성-사용 분리
- 유연하고 재사용 가능한 설계를 원한다면 객체와 관련된 두 가지 책임을 서로 다른 객체로 분해해야 한다. 하나는 객체를 '생성'하는 것이고, 다른 하나는 객체를 '사용'하는 것이다. 한 마디로 말해서 객체에 대한 생성과 사용을 분리해야 한다.
- 사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트로 옮기는 것이다.
<FACTORY 추가하기>
public class Client {
public Money getAvatarFee() {
Movie avatar = new Movie("아바타",
Duration.ofMinutes(120),
Money.wons(10000),
new AmountDiscountPolicy(...));
return avatar.getFee();
}
}
- Client의 코드를 살펴보면 Movie의 인스턴스를 생성하는 동시에 getFee 메시지도 함께 전송함. Client 역시 생성과 사용의 책임을 함께하고 있는 것!
- 이 경우 객체 생성과 관련된 책임만 전담하는 별도의 객체를 추가하고 client는 이 객체를 사용하도록 만들 수 있다. 이처럼 생성과 사용을 분리하기 위해 객체 생성에 특화된 객체를 FACTORY라고 부른다.
public class Factory {
public Money 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();
}
}
<순수한 가공물에게 책임 할당하기>
- PURE FABRICATION (순수한 가공물) : 책임을 할당하기 위해 창조되는 도메인과 무관한 인공적인 객체. INFORMATION EXPERT 패턴에 따라 책임을 할당한 결과가 바람직하지 않을 경우 대안으로 사용된다. 어떤 객체가 책임을 수행하는 데 필요한 많은 정보를 가졌지만 해당 책임을 할당할 경우 응집도가 낮아지고 결합도가 높아진다면 가공의 객체를 추가해서 책임을 옮기는 것을 고민하라. 순수한 가공물(pure fabrication)이라는 표현은 적절한 대안이 없을 때 사람들이 창조적인 무언가를 만들어낸다는 것을 의미하는 관용적인 표현이다.
3. 의존성 주입
: 사용하는 객체가 아닌 외부의 독립적인 객체가 인스턴스를 생성한 후 이를 전달해서 의존성을 해결하는 방법.
- 생성자 주입 : 객체를 생성하는 시점에 생성자를 통한 의존성 해결
- setter 주입 : 객체 생성 후 setter 메서드를 통한 의존성 해결
- 메서드 주입 : 메서드 실행 시 인자를 이용한 의존성 해결
4. 의존성 역전 원칙
<추상화와 의존성 역전>
- 객체 사이의 협력이 존재할 때 그 협력의 본질을 담고 있는 것은 상위 수준의 정책이다. 그러나 이런 상위 수준의 클래스가 하위 수준의 클래스에 의존한다면 하위 수준의 변경에 의해 상위 수준 클래스가 영향을 받게 될 것이다.
- 상위 수준의 클래스와 하위 수준의 클래스 모두 추상화에 의존하도록 하라. 그러면 하위 수준의 클래스의 변경으로 인해 상위 수준의 클래스가 영향을 받는 것을 방지할 수 있다. 또한 상위 수준을 재사용할 때 하위 수준의 클래스에 얽매이지 않고도 다양한 컨텍스트에서 재사용이 가능하다.
- 의존성 역전 원칙
- 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다. 둘 모두 추상화에 의존해야 한다.
- 추상화는 구체적인 사항에 의존해서는 안된다. 구체적인 사항은 추상화에 의존해야 한다.
<의존성 역전 원칙과 패키지>
- 유연하고 재사용 가능하며 컨텍스트에 독립적인 설계는 전통적인 패러다임이 고수하는 의존성 방향을 역전시킨다. 전통적인 패러다임에서는 상위 수준 모듈이 하위 수준 모듈에 의존했다면 객체지향 패러다임에서는 상위 수준 모듈과 하위 수준 모듈이 모두 추상화에 의존한다. 전통적인 패러다임에서는 인터페이스가 하위 수준 모듈에 속했다면 객체지향 패러다임에서는 인터페이스가 상위 수준 모듈에 속한다.
- 훌륭한 객체지향 설계를 위해서는 의존성을 역전시켜야 한다. 그리고 의존성을 역전시켜야만 유연하고 재사용 가능한 설계를 얻을 수 있다.
5. 유연성에 대한 조언
<유연한 설계는 유연성이 필요할때만 옳다>
- 유연하고 재사용 가능한 설계 : 런타임 의존성과 컴파일 타임 의존성의 차이를 인식하고 동일한 컴파일 타임 의존성으로부터 다양한 런타임 의존성을 만들 수 있는 코드 구조를 가지는 설계. 설계가 복잡함.
- 불필요한 유연성을 불필요한 복잡성을 낳는다. 단순하고 명확한 해법이 그런대로 만족스럽다면 유연성을 제거하라. 유연성은 코드를 읽는 사람들이 복잡함을 수용할 수 있을 때만 가치가 있다. 하지만 복잡성에 대한 걱정보다 유연하고 재사용 가능한 설계의 필요성이 더 크다면 코드의 구조와 실행 구조를 만들어라.
<협력과 책임이 중요하다>
- 초보자가 자주 저지르는 실수 중 하나는 객체의 역할과 책임이 자리를 잡기 전에 너무 성급하게 객체 생성에 집중하는 것이다. 이것은 객체 생성과 관련된 불필요한 세부사항에 객체를 결합시킨다. 객체를 생성할 책임을 담당할 객체나 객체 생성 메커니즘을 결정하는 시점은 책임 할당의 마지막 단계로 미뤄야만 한다. 중요한 비즈니스 로직을 처리하기 위해 책임을 할당하고 협력의 균형을 맞추는 것이 객체 생성에 관한 책임을 할당하는 것보다 우선이다. 책임 관점에서 객체들 간에 균형이 잡혀있는 상태라면 생성과 관련된 책임을 지게 될 객체를 선택하는 것은 간단한 작업이 된다.
'📖 독서 > 오브젝트(Objects)' 카테고리의 다른 글
[오브젝트] 11장. 합성과 유연한 설계 (0) | 2022.04.11 |
---|---|
[오브젝트] 10장. 상속과 코드 재사용 (0) | 2022.04.11 |
[ 오브젝트] 8장. 의존성 관리하기 (0) | 2022.04.09 |
[오브젝트] 7장. 객체 분해 (0) | 2022.04.08 |
[오브젝트] 6장. 메시지와 인터페이스 (0) | 2022.04.04 |