본문 바로가기
Design pattern

데코레이터 패턴(decorator pattern) 정리

by 평범한 개발자... 2020. 7. 6.
데코레이터 패턴(decorator pattern)

데코레이터 패턴

  • 비교적 배우기 쉬운 패턴
  • 상속을 남용하는 전형적인 예를 살펴보고, 실행중에 클래스를 꾸미는(데코레이션) 방법을 배워봅시다.
  • 데코레이터 패턴은 말 그대로 객체를 이렇게 저렇게 꾸미는 패턴입니다

1. 스타벅스

  • 스타벅스의 커피는 메뉴도 다양하고 고객의 기호에 따라 메뉴를 커스터마이징(두유 변경, 샷 추가, 휘핑 추가, 스팀밀크 추가 등)을 할 수 있습니다.
  • 이런 스타벅스 커피 시스템을 코드로 구현해봅시다.

초창기 스타벅스

  • 초창기에는 메뉴가 다양하지 않았습니다.

  • 그래서 음료의 공통사항을 묶은 Beverage 추상 클래스를 만들어서 해당 클래스를 상속 받도록 구현했죠

    • 각 음료의 설명을 담을 변수 description과 음료 가격을 리턴하는 cost() 메소드가 있습니다.

image

  • 처음에는 클래스가 5개 였습니다.

  • 그러나 스타벅스가 점점 발전하면서 스팀우유, 두유, 모카, 휘핑 크림 등을 추가할 수 있게 되었습니다.

  • 그래서 조합할 수 있는 모든 종류의 음료 클래스들을 만들었죠. 처음엔 이랬습니다.

    • Decaf

      • DecafWithWhip

        • DecafWithWhipAndSoy
        • DecafWithWhipAndMocha
        • DecafWithWhipAndSteamedMilk
      • DecafWithSteamedMilk

        • DeafWithSteamedMilkAndMocha
        • DeafWithSteamedMilkAndSoy
      • DecafWithMocha

        • DecafWithMochaAndSoy
      • DecafWithSoy

    • DarkRoast

      • 기타 등등..
    • Espresso

      • 기타 등등..
    • HouseBlend

      • 기타 등등..

         

  • 만약 우유의 가격이 오르거나 새로운 토핑을 추가하면 어떻게 될까요?

    • 우유가 포함된 모든 클래스를 수정해야 하고
    • 새로운 토핑에 대한 수 많은 조합의 클래스를 생성해야합니다.

스타벅스 시스템 개선

늦기 전에 시스템을 얼른 수정해야합니다.

  • 토핑에 대한 내용을 클래스로 만들지 않고, 인스턴스 변수로 만들어서 관리해 봅시다.
  • 그리고 cost() 메소드를 추상메소드로 만들지 않고 추가 토핑 비용을 합산한 가격을 리턴하도록 만들어 보죠.

image

 

  • 음료 클래스 1개와 하위 메뉴 클래스 4개 총 5개의 클래스로, 가격도 쉽게 변경 가능하고 재료도 쉽게 넣거나 뺄 수 있게 되었습니다.
  • 이러한 방식에는 문제가 없을까요?

여전히 문제는 존재한다

  • 우유, 모카와 같은 첨가물의 종류가 증가할때마다 새로운 멤버 인스턴스와 메소드를 추가해야하고, 슈퍼클래스의 cost() 메소드를 수정해야합니다.

  • 새로운 음료가 추가되었는데, 그 중에는 특정 첨가물이 들어가면 안 되는 경우도 있습니다.

    • 아이스티로 예를 들면 Tea 서브클래스에서 슈퍼클래스 메소드 hasWhip()같은 메소드를 여전히 상속 받게 될 것입니다.
  • 더블 샷 추가와 같이 첨가물을 2번 넣고 싶으면 어떻게 해야할까요?

2. 데코레이터 패턴

  • 기존 코드는 건드리지 않고, 확장을 통해서 새로운 행동을 간단하게 추가할 수 있도록 하는게 우리의 목표입니다.

  • 객체를 감싸는 방법을 통해서 위와 같은 목표를 달성할 수 있습니다.

  • 우선 데코레이터 패턴의 정의는 다음과 같습니다.

     

    데코레이터 패턴에서는 객체에 추가적인 요건을 동적으로 첨가

    데코레이터는 서브클래스를 만드는 것을 통해서 기능을 유연하게 확장할 수 있는 방법을 제공

 

test

 

  • Decorator는 자신이 장식(데코레이트)할 구성요소와 같은 인터페이스 또는 추상클래스를 구현
  • Decorator의 구상클래스인 ConcereteDecorator 클래스에는 구성요소(Component)에 대한 레퍼런스가 들어있는 인스턴스 변수가 있음

2.1 스타벅스 클래스 구성도

  • 위와 같은 방식으로 같은 개념을 스타벅스 시스템에 적용해봅시다.

Untitled Diagram (1)

 

  • 이런식으로 구성을 하면 레퍼런스 인스턴스 변수를 통해 cost() 메소드 호출 시, 차례대로 첨가물과 음료의 가격을 계산하여 최종 가격을 반환 받도록 구현할 수 있음

    • 예를 들어 모카와 휘핑 크림이 추가된 다크 로스트 커피를 주문했다면 아래와 같이 데코레이트 될 것이다.

      1. DarkRoast 객체를 가져온다.
      2. Mocha 객체로 장식한다.
      3. Whip 객체로 장식한다.
      4. cost() 메소드를 호출한다. (첨가물의 가격을 계산하는 일은 해당 객체들에게 위임된다.)

     

그림1

 

  • 외부에서 cost() 메소드를 호출하면 Whip 인스턴스가 Mocha 인스턴스의 cost()를 호출하고, Mocha 인스턴스는 DarkRoast의 인스턴스를 호출
  • 차례대로 4500, 1000, 300을 반환하여 모두 더하면 4800원이 되도록 cost() 메소드를 구현

2.2 소스코드

  • 코드의 양을 줄이기 위해 아래 소스코드에서는 음료 및 첨가물의 가격을 고정합니다.

2.2.1 추상 클래스

  • 위 구성에서 Beverage 코드는 처음에 만들었던 클래스를 그대로 씁니다.
  • 첨가물 클래스 (데코레이터 클래스)

    • 각 첨가물에 대해 getDescription() 메소드를 구현하도록 만들 계획이라 추상클래스로 재선언

2.2.2 음료 클래스

  • Beverage클래스로 부터 상속받은 description 멤버변수 값 설정
  • 음료 클래스의 cost() 메소드는 가격만 return 하도록 구현

2.2.3 첨가물 클래스

  • 첨가물 클래스에는 감싸고자 하는 Beverage 인스턴스 변수와 그 객체를 설정하기 위한 생성자가 필요
  • 음료에 어떤 첨가물을 추가했는지 설명하기 위해 getDescription() 메소드 구현
  • 음료가격에 첨가물을 추가한 가격을 리턴하도록 cost() 메소드 구현

2.2.4 주문 코드

image

 

  • 데코레이터의 생성 방식은 조금 지저분합니다. 나중에 팩토리패턴과 빌더패턴을 사용하여 만들게 되면 훨씬 깔끔해 질 것입니다.

2.3 데코레이터 패턴 단점

  • 필요한 기능들을 추가하다보면 잡다한 클래스가 너무 많아집니다.

  • 데코레이터 패턴 기반으로 작성된 API를 사용하여 개발해야하는 사람 입장에서는 괴롭죠

    • 처음 우리가 자바 InputStream API를 접했을때를 떠올려봅시다..

3. 자바 I/O와 데코레이터 패턴

image

 

  • 우리가 흔히 알고있는 InputStream은 대표적인 데코레이터 패턴이 적용된 API중 하나입니다.
  • 데코레이터 패턴을 학습하기 전에 저 그림을 보면 단순히 상속관계를 나타낸 그림이라 생각할 것 입니다.
  • 정리하자면, InputStream 클래스는 추상클래스이며 FilterInputStream을 제외한 하위 클래스는 이를 구현한 구상 클래스입니다.
  • FilterInputStream 클래스는 데코레이터 클래스(추상클래스)입니다.
  • 이를 상속 받아 구현한 Buffered, Data, LineNumber, Pushbank 클래스는 InputStream 구상클래스의 데코를 위한 클래스입니다.
반응형

'Design pattern' 카테고리의 다른 글

옵저버 패턴(observer pattern) 정리  (0) 2020.06.17
스트래티지 패턴(strategy pattern) 정리  (1) 2020.05.20

댓글