데코레이터 패턴

  • 객체에 추가 요소를 동적으로 더할 수 있습니다.

  • 데코레이터를 사용하면 서브클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있습니다.

1. 스타벅스 메뉴를 만듭니다.

  • 처음엔 Beverage(음료) 인터페이스를 통해 하우스블렌드, 다크로스트 원두와 디카페인 그리고 에스프레소라는 큰 종류로 나뉘었어요.

  • 그런데, 우유/데운우유/두유/휘핑크림/모카...와 같은 메뉴 추가사항이 늘어납니다. 이때 추가된 메뉴별로 비용이 추가되기 때문에 여기서 상속을 사용하면 서브클래스가 폭발하게 됩니다.

chevron-righthashtag

2. 변수와 상속을 사용해서 변경해보기

  • 부모 클래스에 각 추가옵션의 Bool 타입을 정의하고, get/set 메소드 또한 정의합니다.

  • 해당 Bool 타입의 여부에 따라서 cost() 메소드를 통해 비용을 추가해줍니다.

  • 자식 클래스에서는 본인 메뉴의 비용 + super.cost()를 호출하여 비용을 추가합니다.

chevron-righthashtag

3. 상속을 사용한 설계의 단점 알아보기

  • 첨가물 가격이 바뀔 때마다 기존 코드를 수정해야 합니다.

  • 첨가물의 종류가 많아지면 새로운 get/set 메소드를 추가해야 하고, 부모클래스의 cost() 메소드 또한 고쳐야 합니다.

  • 새로운 음료가 나올 수 있어요. 아이스티같은. 근데 아이스티는 hasWhip()같은 메소드는 상속받지 않아야 하지만, 상속을 받을 것입니다. 이건 Strategy Pattern을 공부할 때도 말했듯이, 고무 오리가 날아다닌다던지 하는 예상치 못한 치명적인 오류를 발생시킬 수 있어요.

  • 고객이 더블 모카를 주문하면 어떻게 하나요 ? ( 현재는 hasMocha()로만 작업되어있으므로 주문 불가하죠. )

  • 결론적으로 우리의 목표는 기존 코드는 건드리지 않고, 확장으로만 새로운 행동을 추가하는 것입니다.

4. 데코레이터 패턴 설계하기 1

  • Decorator(장식)의 슈퍼클래스는 자신이 장식하고 있는 객체의 슈퍼클래스와 같습니다.

  • 한 객체를 여러 개의 데코레이터로 감쌀 수 있습니다.

  • Decorator(장식)는 자신이 감싸고 있는 객체와 같은 슈퍼클래스를 가지고 있기에 원래 객체가 들어갈 자리에 데코레이터 객체를 넣어도 상관없습니다.

  • Decorator(장식)는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업을 수행할 수 있습니다.

  • 객체는 언제든지 감쌀 수 있으므로 실행 중에 필요한 데코레이터를 마음대로 적용할 수 있습니다.

5. 데코레이터 패턴 설계하기 2

  • 이해를 돕기위해 다이어그램을 참고하자.

  • 중요한 CondimentDecorator에서 Beverage를 상속하고 있다는 점과 구성(Composition)으로 Beverage를 가지고 있다는 점이다.

  • 위에서 상속 안좋다고 그러고 왜 상속을 쓰냐고 할 수도 있다만 이건 다른 너낌이다. 왜냐면 이따가 코드를 보면 알겠지만, Berverage 객체를 생성하고 그 녀석을 Decorator(장식)로 감싸서 할당해주기 때문이다.

  • 즉, 기존의 상속처럼 부모 클래스의 행동을 상속하기 위한 상속이 아니라 생성한 Berverage 객체에 데코레이터로 감싼 객체가 들어가야되서 상속을 받고 있는 것이다.

  • 그리고 또 다이어그램을 보면 있는 우유, 모카, 두유, 휘핑크림같은 Decorator(장식)들이 CondimentDecorator를 상속한다.

  • 이건 무슨의미냐면, CondimentDecorator는 부모 클래스인 Berverage 객체를 구성으로 가지고 있다. 그리고 메뉴 주문에 따라 모카로 래핑, 모카 또 래핑, 휘핑 래핑, 우유 래핑... 같은 식으로 계속 래핑이 되는데 최종 비용을 계산하려면 Berverage 객체가 필요하다.

  • 자세한 내용은 아래의 구현된 코드를 보자.

6. 데코레이터 패턴 구현하기

  • Beverage 클래스가 있고, 서브 클래스로 HouseBlend, DarkRoast, Decaf, Espresso가 있다. 이 클래스들은 내부에 description, cost() 이 구현되어 있다.

  • CondimentDecorator 는 Beverage 을 상속하며, Beverage 객체를 Composition(구성) 으로 가지고 있다. 따라서 Beverage 혹은 Beverage 의 서브 클래스들에 접근이 가능하다.

  • Decorator(장식)들은 모카, 두유, 우유, 휘핑크림이 있는데 CondimentDecorator를 상속하고 있고 생성자를 통해 Beverage 를 받는다.

  • 또, Decorator(장식)들은 getDescription(), cost()을 Beverage 객체를 통해서 누적으로 계산할 수 있다.

chevron-righthashtag

7. 데코레이터 패턴 정리하기

  • 아래의 코드는 아래의 그림을 코드로 구현해서 실행하는 부분이다.

  • 그림을 보면 계속 모카, 휘핑 크림 추가 처럼 첨가물을 계속 실행 중에 추가할 수 있게 되어 있다.

  • 왜? 데코레이터가 Berverage를 상속해서 타입이 같고, 모카/우유 애들은 CondimentDecorator를 상속하고 있으니까 그렇다. 즉, 상속을 행동을 받으려고 한 것이 아니라 타입을 맞추려고 한 것이다.

  • 또, 생성자로 구성으로 가지고 있는 beverage을 입력받아 기존에 래핑 되어있는 데이터에 접근이 가능해서 그 값에 추가해서 최종결과를 리턴해준다.

  • 이런식이면, 예를들어 첨가물이 새롭게 추가된다고 했을 때 기존 코드는 건드리지 않고 데코레이터를 상속한 첨가물을 새로 만들면 된다. 그래서 실행 중에 마음대로 조합해서 사용할 수 있다.

  • 즉, 목표대로 기존 코드는 건드리지 않고(변경에는 닫혀있고), 새로운 행동을 추가(확장에는 열려있다)할 수 있는 것이다.

  • 유의할 점은 아래의 코드에 나와 있는 것 처럼 한 번 DarkRoast를 Beverage로 감싸고 나면, 그게 DarkRoast인지 뭔지 알 수가 없다는 부분이다.

chevron-righthashtag

8. 커피 메뉴에 사이즈 추가하기

  • 커피에 Tall, Grande, Venti 사이즈가 새로 생겼다.

  • 그리고 사이즈에 따라 첨가물도 그만큼 양이 추가될 것이므로 첨가물 가격도 다르게 받으려고 한다.

chevron-righthashtag

9. 개방 폐쇄 원칙(OCP)

  • 클래스는 확장에는 열려있어야 하지만 변경에는 닫혀 있어야 한다는 원칙이다(Open Closed Principle).

  • 위에서 본 것처럼 유연성 면에서 보면 상속으로 확장하는 일은 별로 좋은 선택이 아니다.

  • 데코레이터 패턴을 사용하면 구성과 위임으로 실행 중에 새로운 행동을 추가할 수도 있다.

  • 데코레이터 패턴의 예시처럼 어떤 메뉴나 옵션이 하나 추가됐다고하면 기존 코드를 건드리지 않고, 그 옵션에 해당하는 데코레이터를 새로 만들어서 기존의 음료에 감싸주기만 하면 된다.

Reference

https://www.hanbit.co.kr/store/books/look.php?p_code=B6113501223arrow-up-right

Last updated