데코레이터 패턴
Last updated
Last updated
객체에 추가 요소를 동적으로 더할 수 있습니다.
데코레이터를 사용하면 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있습니다.
처음엔 Beverage(음료) 인터페이스를 통해 하우스블렌드, 다크로스트 원두와 디카페인 그리고 에스프레소라는 큰 종류로 나뉘었어요.
그런데, 우유/데운우유/두유/휘핑크림/모카...와 같은 메뉴 추가사항이 늘어납니다. 이때 추가된 메뉴별로 비용이 추가되기 때문에 여기서 상속을 사용하면 서브클래스가 폭발하게 됩니다.
부모 클래스에 각 추가옵션의 Bool 타입을 정의하고, get/set 메소드 또한 정의합니다.
해당 Bool 타입의 여부에 따라서 cost() 메소드를 통해 비용을 추가해줍니다.
자식 클래스에서는 본인 메뉴의 비용 + super.cost()를 호출하여 비용을 추가합니다.
첨가물 가격이 바뀔 때마다 기존 코드를 수정해야 합니다.
첨가물의 종류가 많아지면 새로운 get/set 메소드를 추가해야 하고, 부모클래스의 cost() 메소드 또한 고쳐야 합니다.
새로운 음료가 나올 수 있어요. 아이스티같은. 근데 아이스티는 hasWhip()같은 메소드는 상속받지 않아야 하지만, 상속을 받을 것입니다. 이건 Strategy Pattern을 공부할 때도 말했듯이, 고무 오리가 날아다닌다던지 하는 예상치 못한 치명적인 오류를 발생시킬 수 있어요.
고객이 더블 모카를 주문하면 어떻게 하나요 ? ( 현재는 hasMocha()로만 작업되어있으므로 주문 불가하죠. )
결론적으로 우리의 목표는 기존 코드는 건드리지 않고, 확장으로만 새로운 행동을 추가하는 것입니다.
Decorator(장식)의 슈퍼클래스는 자신이 장식하고 있는 객체의 슈퍼클래스와 같습니다.
한 객체를 여러 개의 데코레이터로 감쌀 수 있습니다.
Decorator(장식)는 자신이 감싸고 있는 객체와 같은 슈퍼클래스를 가지고 있기에 원래 객체가 들어갈 자리에 데코레이터 객체를 넣어도 상관없습니다.
Decorator(장식)는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업을 수행할 수 있습니다.
객체는 언제든지 감쌀 수 있으므로 실행 중에 필요한 데코레이터를 마음대로 적용할 수 있습니다.
이해를 돕기위해 다이어그램을 참고하자.
중요한 CondimentDecorator에서 Beverage를 상속하고 있다는 점과 구성(Composition)으로 Beverage를 가지고 있다는 점이다.
위에서 상속 안좋다고 그러고 왜 상속을 쓰냐고 할 수도 있다만 이건 다른 너낌이다. 왜냐면 이따가 코드를 보면 알겠지만, Berverage 객체를 생성하고 그 녀석을 Decorator(장식)로 감싸서 할당해주기 때문이다.
즉, 기존의 상속처럼 부모 클래스의 행동을 상속하기 위한 상속이 아니라 생성한 Berverage 객체에 데코레이터로 감싼 객체가 들어가야되서 상속을 받고 있는 것이다.
그리고 또 다이어그램을 보면 있는 우유, 모카, 두유, 휘핑크림같은 Decorator(장식)들이 CondimentDecorator를 상속한다.
이건 무슨의미냐면, CondimentDecorator는 부모 클래스인 Berverage 객체를 구성으로 가지고 있다. 그리고 메뉴 주문에 따라 모카로 래핑, 모카 또 래핑, 휘핑 래핑, 우유 래핑... 같은 식으로 계속 래핑이 되는데 최종 비용을 계산하려면 Berverage 객체가 필요하다.
자세한 내용은 아래의 구현된 코드를 보자.
Beverage 클래스가 있고, 서브 클래스로 HouseBlend, DarkRoast, Decaf, Espresso가 있다. 이 클래스들은 내부에 description, cost() 이 구현되어 있다.
CondimentDecorator 는 Beverage 을 상속하며, Beverage 객체를 Composition(구성) 으로 가지고 있다. 따라서 Beverage 혹은 Beverage 의 서브 클래스들에 접근이 가능하다.
Decorator(장식)들은 모카, 두유, 우유, 휘핑크림이 있는데 CondimentDecorator를 상속하고 있고 생성자를 통해 Beverage 를 받는다.
또, Decorator(장식)들은 getDescription(), cost()을 Beverage 객체를 통해서 누적으로 계산할 수 있다.
아래의 코드는 아래의 그림을 코드로 구현해서 실행하는 부분이다.
그림을 보면 계속 모카, 휘핑 크림 추가 처럼 첨가물을 계속 실행 중에 추가할 수 있게 되어 있다.
왜? 데코레이터가 Berverage를 상속해서 타입이 같고, 모카/우유 애들은 CondimentDecorator를 상속하고 있으니까 그렇다. 즉, 상속을 행동을 받으려고 한 것이 아니라 타입을 맞추려고 한 것이다.
또, 생성자로 구성으로 가지고 있는 beverage을 입력받아 기존에 래핑 되어있는 데이터에 접근이 가능해서 그 값에 추가해서 최종결과를 리턴해준다.
이런식이면, 예를들어 첨가물이 새롭게 추가된다고 했을 때 기존 코드는 건드리지 않고 데코레이터를 상속한 첨가물을 새로 만들면 된다. 그래서 실행 중에 마음대로 조합해서 사용할 수 있다.
즉, 목표대로 기존 코드는 건드리지 않고(변경에는 닫혀있고), 새로운 행동을 추가(확장에는 열려있다)할 수 있는 것이다.
유의할 점은 아래의 코드에 나와 있는 것 처럼 한 번 DarkRoast를 Beverage로 감싸고 나면, 그게 DarkRoast인지 뭔지 알 수가 없다는 부분이다.
커피에 Tall, Grande, Venti 사이즈가 새로 생겼다.
그리고 사이즈에 따라 첨가물도 그만큼 양이 추가될 것이므로 첨가물 가격도 다르게 받으려고 한다.
클래스는 확장에는 열려있어야 하지만 변경에는 닫혀 있어야 한다는 원칙이다(Open Closed Principle).
위에서 본 것처럼 유연성 면에서 보면 상속으로 확장하는 일은 별로 좋은 선택이 아니다.
데코레이터 패턴을 사용하면 구성과 위임으로 실행 중에 새로운 행동을 추가할 수도 있다.
데코레이터 패턴의 예시처럼 어떤 메뉴나 옵션이 하나 추가됐다고하면 기존 코드를 건드리지 않고, 그 옵션에 해당하는 데코레이터를 새로 만들어서 기존의 음료에 감싸주기만 하면 된다.
https://www.hanbit.co.kr/store/books/look.php?p_code=B6113501223