01 개방-폐쇄 원칙
개방-폐쇄 원칙(Open-Closed Principle, OCP)란? 소프트웨어 개체는 확장에 대해 열려있고, 수정에 대해서는 닫혀있어야한다는 원칙이다.
OCP은 유연한 설계란 기존의 코드를 수정하지 않고고도 애플리케이션의 동작을 확장시킬 수 있는 설계라고 이야기한다.
그렇지만 일반적으로 애플리케이션의 동작을 확장하기 위해서는 코드를 수정한다.. 어떻게 코드를 수정하지않고 확장을 시킬 수 있을까?
그 방법중 하나는 이전 장에서 살펴봤듯이 추상클래스를 정의하여 구체적인 새로운 타입을 추가하는 방법을 말할것이다. 이번장이 앞장의 내용이 반복된다는 느낌을 받을 수 있겠지만 앞서 언급한 기법들을 OCP와 같은 원칙이라는 관점에서 정리할 것이다.
컴파일타임 의존성을 고정시키고 런타임 의존성을 변경하라
사실 OCP는 런타임 의존성과 컴파일타임 의존성에 관한 이야기다. 앞장의 할인 정책을 의존성의 관점에서 다시 살펴보면
런타임 의존성과 컴파일타임 의존성이 동일하지 않다고 이야기했다.
할인 정책 설계는 이미 OCP 원칙을 따르고 있다.
여러 할인 정책을 동시에 적용할 수 있는 할인 정책을 추가했던것을 떠올려보자. 이를 위해 한 일은 DiscountPolicy의 자식 클래스로 OverlappedDiscountPolicy 클래스를 추가한 것뿐이다.
결과적으로 기존 코드를 수정하지 않고 애플리케이션의 동작을 확장시켰다. 즉 할인 정책은 확장에는 열려있고 수정에는 닫혀있게끔 설계되어 있다.
추상화가 핵심이다
OCP의 핵심은 추상화에 의존하는 것이다.
추상화란? 핵심적인(공통적인)인 부분만 남기고 문맥에 따라 변경될 수 있는 부분은 생략함으로써 복잡성을 극복하는 기법이다. 즉, 공통적인 부분은 문맥이 바뀌더라도 변하지 않아야한다. 따라서 추상화된 부분은 수정에 대해 닫혀있고 변경될 여지가 있어 생략된 부분은 확장의 여지를 남긴다
단순히 어떤 개념을 추상화헀다고 해서 수정에 대해 닫혀 있는 설계를 만들 수 있는 것은 아니다. 수정에 대한 영향을 최소화하기 위해서는 모든 요소가 추상화에 의존해야한다.
앞서 장들에 등장했던 Movie 클래스를 살펴보자.
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie(DiscountPolicy discountPolicy) {
...
this.discountPolicy = discountPolicy;
}
public Money calulateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening);
}
}
할인 정책을 판단해서 요금을 계산하기 위해 Movie는 추상클래스인 DiscountPolicy를 의존한다. 의존성은 변경의 영향을 의미하지만 DiscountPolicy는 변하지 않는 추상화라는 사실에 주목해야한다. 딸하서 DiscountPolicy의 자식 클래스를 추가하더라도 영향을 받지 않는다. -> Movie와 DiscountPolicy는 수정에 대해 닫혀 있다.
02 생성 사용 분리
public class Movie {
...
private DiscountPolicy discountPolicy;
public Movie() {
...
this.discountPolicy = new AmountDiscountPolicy(...);
}
public Money calulateMovieFee(Screening screening) {
return fee.minus(discountPolicy.calculateDiscountAmount(screening);
}
}
위의 코드에서 할인 정책을 비율 할인 정책으로 사용하기위해 변경하는 방법은 AmountDiscountPolicy대신 PercentDiscountPolicy로 생성하도록 코드를 수정하는 것뿐이다. 이는 기존 코드를 수정하기 때문에 OCP를 위반한다.
메세지를 전송하지 않고 객체를 생성하기만 한다면 아무런 문제가 없을 것이다. 객체를 생성하지 않고 메세지만 전송한다면 이 또한 괜찮을 것이다. 동일한 클래스 안에서 객체 생성과 사용이라는 두 가지 이질적인 목적을 가진 코드가 공존하는 것이 문제인 것이다.
사용으로부터 생성을 분리하는 데 사용되는 가장 보편적인 방법은 객체를 생성할 책임을 클라이언트에게 위임하는 것이다.
public class Client {
public Money getAvatarFee() {
Movie avatar = new Movie(new AmountDiscountPolicy(...));
return avatar.getFee();
}
}
Movie의 의존성을 추상화인 DiscountPolicy로만 제한하기 때문에 확장에 대해서는 열려 있으면서도 수정에 대해서는 닫혀 있는 코드를 만들 수 있다.
FACTORY 추가하기
생성 책임을 Client로 옮긴 배경에는 Movie는 특정 컨텍스트에 묶여서는 안 되지만 Client는 묶여도 상관 없다는 전제가 깔려 있다. 하지만 Client 또한 특정 컨텍스트에 묶이지 않기 바란다면 Factory 객체를 추가해보자.
public class Factory {
public Money createAvatarMovie() {
return new Movie(new AmountDiscountPolicy(...));
}
}
public class Client {
private Factory factory;
public Client(Factory factory) {
this.factory = factory;
}
public Money getAvatarFee() {
Movie avatar = factory.createMadMaxMovie();
return avatar.getFee();
}
}
순수한 가공물에게 책임 할당하기
크레이그 라만은 시스템을 객체로 분해하는 데는 크게 두 가지 표현적 분해(representational decomposition), 행위적 분해(behavioral decomposition) 방식이 존재한다고 설명한다.
표현적 분해는 도메인에 존재하는 사물 또는 개념을 표현하는 객채들을 이용해서 시스템을 분해하는 것이고 도메인과 소프트웨어 사이의 표현적 차이를 최소화하는것을 목적으로 하는 방식이다.
도메인 모델은 설계를 위한 중요한 출발점이지만 실제로 애플리케이션은 도메인 예를 들어 DAO와 같이 도메인 개념을 초월하는 기계적인 개념들을 필요로 한다.
어떠한 행동을 추가하려 하는데 이 행동을 책임질 어떠한 도메인 개념이 존재하지 않는다면 도메인과 무관한 인공적인 객체인 PURE FABRICATION(순수한 가공물)을 추가하고(=DAO, FACTORY) 이 객체에게 책임을 할당하면 된다.
PURE FABRICATION은 행위적 분해에 의해 생성되는 것이 일반적이다.
정리 하자면 우리가 애플리케이션을 구축하는 것은 사용자들이 원하는 기능을 제공하기 위한것이지 실세계를 모방하거나 시물레이션하기 위한 것이 아니다. 도메인개념을 표현하는 추상화를 이용해 애플리케이션을 구축하기 시작하되, 만약 도메인 개념이 만족스럽지 못하다면 주저말고 인공적인 객체를 창조하면된다. 😁
03 의존성 주입
의존성을 해결하는 세가지 의존성 주입 방법이 존재한다. (이미 앞장에서 설명했지만)
- 생성자 주입: 객체를 생성하는 시점에 생성자를 통한 의존성 해결 (어떤 의존성이 필수적인지 명시적임)
- setter 주입: 객체 생성 후 setter 메서드를 통한 의존성 해결 (런타임에 의존성 변경가능하지만 어떤 의존성이 필수적인지 명시되지 않음.)
- 메서드 주입: 메서드 실행 시 인자를 이용한 의존성 해결 (생성자를 통해 주입된 의존성이 한 두 개의 메서드에서만 사용된다면 각 메서드의 인자로 전달하는 메서드 주입이 더 나은 방법일 수 있다.)
숨겨진 의존성은 나쁘다
의존성 주입 외에도 의존성을 해결하는 다양한 방법이 존재한다. 널리 사용되는 대표적인 방법인 SERVICE LOCATOR 패턴이 존재한다.
하지만 저자는 개인적으로 SERVICE LOCATOR를 선호하지 않는다. 이유는 SERVICE LOCATOR 패턴은 의존성을 내부로 감추기 때문에 숨겨진 의존성을 이해하기 어렵다. 개발자가 내부의 의존성을 이해한다는것은 캡슐화 또한 위반한다는 것으로도 이어질 수 있다.
그러므로 SERVICE LOCATOR에 대해서는 살펴보지 않을것이며 숨겨진 의존성보다는 명시적인 의존성을 지향하도록 하는것으로 간단히 결론을 내고자한다.
04 의존성 역전 원칙
추상화와 의존성 역전
- Movi와 같은 상위 수준의 모듈은 AmountDiscountPolicy, PercentDiscountPolicy와 같은 하위 수준의 모듈에 의존해서는 안되며 둘 모두 추상화에 의존해야한다.
- 추상화는 구체적인 사항에 의존해서는 안되며 구체적인 사항이 추상화를 의존해야한다.
이를 의존성 역전 원칙(Dependency Inversion Principle, DIP) 라고 부른다.
Martin 피셜에 따르면 '역전' 이란? 전통적인 소프트웨어 개발 방법에서는 상위 수준의 모듈이 하위 수준의 모듈을 의존하는 것이 일반적이였지만 잘 설계된 객체지향 프로그램의 의존성 구조는 전통적인 절차적 방법에 의해 일반적으로 만들어진 의존성 구조에 대해 '역전'(추상적인것을 의존) 한것이다.
의존성 역전 원칙과 패키지
위 그림과 같은 구조로 패키지가 구성되어 있을 때 Movie를 다양한 컨텍스트에서 재사용하기 위해서는 Movie 컴파일 시 불필요할 수 있는 DiscountPolicy클래스가 필요할 수도 있다.
따라서 위의 그림과 같이 DiscountPolicy와 같은 추상화를 별도의 독립적인 패키지가 아닌 클라이언트가 속한 패키지에 포함시켜야 한다. Movie를 다른 컨텍스트에서 재사용하기 위해서s는 단지 Movie와 DiscountPolicy가 포함된 패키지만 재사용하면 된다. 마틴 파울러는 이 기법을 가리켜 SEPARATED INTERFACE 패턴이라고 부른다.
의존성 역전 원칙에 따라 상위 수준의 협력 흐름을 재사용하기 위해서는 추상화가 제공하는 인터페이스의 소유권역시 역전시켜야한다.
05 유연성에 대한 조건
유연한 설계는 유연성이 필요할 때만 옳다
유연한 설계라는 말의 이면에는 복잡한 설계라는 의미가 숨어 있다. 유연한 설계의 이런 양면성은 객관적으로 설계를 판단하기 어렵게 만든다. 유연하고 재사용 가능한 설계는 컴파일타임 의존성으로 부터 다양한 런타임 의존성을 만들 수 있는 코드 구조를 가지기 때문이다.
반대로 유연하지 않은 설계는 단순하고 명확하다.
불필요한 유연성은 불필요한 복잡성을 낳기때문에 코드를 읽는 사람이 복잡함을 수용할 수 있을 때만 가치가 있다. 즉, 복잡성에 대한 걱정보다 유연하고 재사용 가능한 설계의 필요성이 더 크다면 구조와 실행 구조를 다르게 만들면된다.
협력과 책임이 중요하다
설계를 유연하게 만들기 위해서는 먼저 역할, 책임, 협력에 초점을 맞춰야 한다.
객체들이 메세지 전송자의 관점에서 동일한 책임을 수행한다면 공통의 추상화를 도출 하며 모두 협력에 참여하며 책임을 수행하게하며 동일한 역할을 수행할 수 있게 만든다.
다양한 컨텍스트에서 협력을 재사용할 필요가 없다면 설계를 유연하게 만들 당위성도 함께 사라진다.
'📚 Book > Object' 카테고리의 다른 글
11 합성과 유연한 설계 (0) | 2020.03.29 |
---|---|
10 상속과 코드 재사용 (0) | 2020.03.22 |
08 의존성 관리하기 (0) | 2020.03.08 |
07 객체 분해 (0) | 2020.02.29 |
06 메세지와 인터페이스 (0) | 2020.02.19 |