

OCP 살펴보기 (p120)
OCP(Open-Closed Principle)는 정말 중요한 디자인 원칙 중 하나입니다.
디자인 원칙 - 클래스에는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.
우리의 목표는 기존 코드를 건드리지 않고 확장으로 새로운 행동을 추가하는 것입니다. 이 목표를 달성했을 때 무엇을 얻을 수 있을까요?
새로운 기능을 추가할 때 급변하는 주변 환경에 잘 적응하는 유연하고 튼튼한 디자인을 만들 수 있겠죠!
Q1. '확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.'는 문장이 뭔가 모순 같아요
어떻게 그 2가지 조건을 동시에 만족할 수 있는 거죠?
처음에는 모순처럼 보일 수 있습니다. 변경하기 힘들다면 확장하기도 힘들 테니까요
하지만 코드를 변경하지 않아도 시스템을 확장하게 해 주는 기발한 객체지향 기법은 많습니다.
2장에서 배웠던 옵저버 패턴을 생각해보죠. 옵저버를 새로 추가하면 주제에 코드를 추가하지 않으면서도 얼마든지 확장할 수 있습니다.
객체지향 디자인 기법을 배우다 보면 행동들을 확장할 수 있는 방법이 많다는 사실을 자연스럽게 알게 됩니다.
Q2. 옵저버 패턴은 이해했는데, 코드를 변경하지 않으면서 확장이 용이한 디자인을 쉽게 만들 수 있나요?
확장하려고 코드를 직접 수정하는 일을 방지하는 디자인 기법이 있습니다.
그런 기법은 대부분 오랜 시간을 걸쳐서 검증 받은 것들이죠. 3장에서는 데코레이터 패턴으로 OCP를 준수하는 방법을 배웁니다.
데코레이터 패턴 살펴보기 (p122)
상속을 써서 음료 가격과 첨가물(샷, 시럽, 우유, 휘핑크림 등) 가격을 합해서 총 가격을 산출하는 방법은 그리 좋은 방법이 아니었습니다.
클래스가 어마어마하게 많아지거나 일부 서브클래스에는 적합하지 않은 기능을 추가해야 하는 문제가 있었죠.
다른 방법을 한번 생각해 봅시다. 일단 특정 음료에서 시작해서 첨가물로 그 음료를 장식해볼까요?
예를 들어 어떤 고객이 모카와 휘핑크림을 추가한 다크 로스트 커피를 주문한다면 다음과 같이 장식할 수 있습니다.
- DarkRoast 객체를 가져온다.
- Mocha 객체로 장식한다.
- Whip 객체로 장식한다.
- cost() 메소드를 호출한다.
이때 첨가물의 가격을 계산하는 일은 해당 객체에게 위임한다.
여기서 객체를 어떻게 장식할 수 있을까요? 그리고 이 과정에서 어떤 식으로 위임할 수 있을까요?
주문 시스템에 데코레이터 패턴 적용하기 (p123)
- DarkRoast 객체에서 시작합니다.
1.1 DarkRoast는 Beverage로부터 상속받으므로 음료의 가격을 계산하는 메소드를 가지고 있습니다.
- 고객이 모카를 주문했으니까 Mocha 객체를 만들고 그 객체로 DarkRoast를 감쌉니다.
2.1 Mocha 객체는 데코레이터입니다. 객체의 형식은 객체가 장식하고 있는 객체를 반영하는데, 이 경우에는 Beverage가 되겠죠.
여기에서 반영(mirror)한다는 것은 "같은 형식을 갖는다"라는 뜻으로 이해하면 됩니다.
2.2 Mocha에도 cost() 메소드가 있고, Mocha가 감싸고 있는 것도 Beverage 객체로 간주할 수 있습니다.
Mocha도 Beverage의 서브클래스 형식이니까요.
- 고객이 휘핑크림도 추가했으니까 Whip 데코레이터를 만들어 Mocha를 감쌉니다.
3.1 Whip도 데코레이터라서 DarkRoast의 형식을 반영하며 cost() 메소드를 가지고 있습니다.
3.2 Mocha와 Whip에 싸여 있는 DarkRoast는 여선히 Beverage 객체이기에
cost() 메소드 호출을 비롯한, DarkRoast에 관한 일이라면 무엇이든 할 수 있습니다.
- 가격을 구할 때는 가장 바깥쪽에 있는 데코레이터인 Whip의 cost()를 호출하면 됩니다.
그러면 Whip은 그 객체가 장식하고 있는 객체에게 가격 계산을 위임합니다.
가격이 구해지고 나면, 계산된 가격에 휘핑크림의 가격을 더한 다음 결과값을 리턴합니다.
지금까지의 내용 정리 (p124)
데코레이터의 슈퍼클래스는 자신의 장식하고 있는 객체의 슈퍼클래스와 같습니다.
한 객체를 여러 개의 데코레이터로 감쌀 수 있습니다.
데코레이터는 자신이 감싸고 있는 객체와 같은 슈퍼클래스를 가지고 있기에
원래 객체(싸여 있는 객체)가 들어갈 자리에 데코레이터 객체를 넣어도 상관없습니다.
데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업을 수행할 수 있습니다.
객체는 언제든지 감쌀 수 있으므로 실행 중에 필요한 데코레이터를 마음대로 적용할 수 있습니다.
이제 데코레이터 패턴의 정의를 알아본 다음 코드를 살펴보면서 실제로 이 패턴이 어떤 식으로 돌아가는지 살펴보겠습니다.
데코레이터 패턴의 정의 (p125)
데코레이터 패턴(Decorator Pattern)으로 객체에 추가 요소를 동적으로 더할 수 있습니다.
데코레이터를 사용하면 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있습니다.
Q&A (p133)
Q1. 이 코드를 그대로 쓰면 구상 구성 요소로 특별 할인 같은 작업을 처리할 때 문제가 생기지 않을까 걱정입니다. 일단 HouseBlend를 데코레이터로 감싸고 나면 그 커피가 하우스 블렌드인지 다크 로스트인지 알 수 없잖아요.
네. 그렇습니다. 구상 구성 요소로 어떤 작업을 처리하는 코드에 데코레이터 패턴을 적용하면 코드가 제대로 작동하지 않습니다.
반대로 추상 구성 요소로 돌아가는 코드에는 데코레이터 패턴을 적용해야만 제대로 된 결과를 얻을 수 있습니다.
구상 구성 요소로 돌아가는 코드를 만들어야한다면 데코레이터 패턴 사용을 다시 한번 생각해 봐야 합니다.
Q2. 데코레이터를 빼먹거나 순서를 다르게하는 실수를 할 우려는 없나요?
데코레이터 패턴을 쓰면 관리해야 할 객체가 늘어나니까 코딩할 때 실수할 가능성도 높아지겠죠.
실제로는 팩토리나 빌더 같은 다른 패턴으로 데코레이터를 만들고 사용합니다.
나중에 그런 패턴을 배우다 보면 데코레이터로 장식된 구상 구성 요소는 캡슐화가 잘되어 있어서 질문에서 제기한 문제는 별로 걱정하지 않아도 된다는 사실을 알게됩니다.
Q3. 데코레이터가 같은 객체를 감싸고 있는 다른 데코레이터를 알 수 있나요? getDescription() 메소드에서 '모카, 휘핑크림, 모카'라고 출력하는 대신 '휘핑크림, 더블 모카' 같은 식으로 출력하고 싶은데 그러려면 가장 바깥쪽에 있는 데코레이터가 그 안에 있는 다른 데코레이터를 알아야 하잖아요.
데코레이터는 감싸고 있는 객체에 행동을 추가하는 용도로 만들어집니다.
만약 여러 단계의 데코레이터를 파고 들어가서 어떤 작업을 해야한다면 패턴이 만들어진 의도에 어긋납니다.
그렇지만 질문처럼 출력 방식을 바꾸는 방법이 없지는 않습니다. 마지막에 만들어진 description 내용을 파싱해서 문자열을 바꿔주는 데코레이터를 만들면 되겠죠.
getDescription() 메소드에서 ArrayList를 리턴하도록 하면 좀 더 쉽게 작업할 수 있습니다.
패턴 집중 인터뷰 (p138) - 다시 보기
- 디자인을 유연하게 만들수 있지만 자잘한 클래스가 엄청나게 추가되는 경우가 종종 있음.
- 데코레이터를 씨워 넣어도 클라이언트는 데코레이터를 사용하고 있다는 사실을 전혀 알 수 없다는 장점이 있음.
- 하지만 특정 형식에 의존하는 클라이언트 코드에 데코레이터를 그냥 적용하면 모든게 엉망이 된다.
- 데코레이터를 도입하면 구성 요소를 추기화하는 데 필요한 코드가 훨씬 복잡해진다는 단점이 있음
- 구성 요소 인스턴스만 만든다고 해서 끝나는게 아님. 꽤 많은 데코레이터로 감싸야 하는 경우가 종종 있음.
핵심 정리 (p139)
- 객체지향 원칙
- 바뀌는 부분은 캡슐화한다.
- 상속보다는 구성을 활용한다.
- 구현보다는 인터페이스에 맞춰서 프로그래밍 한다.
- 상호작용하는 객체 사이에서는 가능하면 느슨한 결합을 사용해야 한다.
- 클래스는 확장에는 열려있어야 하지만 변경에는 닫혀있어야 한다.(OCP)
- OCP가 추가되었습니다. 시스템을 디자인 할 때는 닫혀있는 부분과 새로 확장되는 부분이 확실하게 구분되도록 노력합니다.
- 객체지향 패턴 - 데코레이터 패턴
- 객체에 추가 요소를 동적으로 더할 수 있습니다.
- 데코레이터를 사용하면 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있습니다.
- 디자인의 유연성 면에서 보면 상속으로 확장하는 일은 별로 좋은 선택이 아닙니다.
- 기존 코드 수정 없이 행동을 확장해야 하는 상황도 있습니다.
- 구성과 위임으로 실행 중에 새로운 행동을 추가할 수 있습니다.
- 데코레이터 패턴은 구상 구성 요소를 감싸 주는 데코레이터를 사용합니다.
- 데코레이터 클래스의 형식은 그 클래스가 감싸는 형식을 반영합니다.
(상속이나 인터페이스 구현으로 자신이 감쌀 클래스와 같은 형식을 가집니다.)
- 구성 요소를 감싸는 데코레이터의 개수에는 제한이 없습니다.
- 구성 요소의 클라이언트는 데코레이터의 존재를 알 수 없습니다.
클라이언트가 구성요소의 구체적인 형식에 의존하는 경우는 예외입니다.
- 데코레이터 패턴을 사용하면 자잘한 객체가 매우 많이 추가될 수 있고, 데코레이터를 너무 많이 사용하면 코드가 필요 이상으로 복잡해집니다.