ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [헤드퍼스트 디자인패턴] 3장. 객체 꾸미기 : 데코레이터 패턴
    Book/헤드 퍼스트 디자인 패턴 2023. 4. 29. 00:50

    3장. 객체 꾸미기: 데코레이터 패턴

    스타버즈 카페(가명)는 모두 알 것이다. 그 카페 사업을 시작 무렴 주문 시스템 클래스는 다음과 같이 구성되었다.

    모든 서브 클래스에서 음료의 가격을 리턴하는 cost 메소드를 구현해야 한다.

     

    고객이 커피를 주문할 때 우유나, 두유, 모카 같은 서브 메뉴를 추가하고 그 위에 휘핑을 얹기도 한다. 각각을 추가할 때마다 커피 가격이 올라가야 하기 때문에 주문 시스템을 구현할 때 이러한 점을 모두 고려해야한다.

     

    그런데 여기서 문제가 발생했다. 점점 패턴이 커지면서 클래스가 '폭발'하게 된것이다.

    각 서브 클래스의 cost()메소드는 첨가물도 포함해서 커피의 최종 가격을 계산한다.

     

    그러면 '우리는 클래스가 이렇게까지 필요할 이유가 없고, 그냥 인스턴스 변수와 슈퍼 클래스 상속을 써서 첨가물을 관리할 수도 있지 않는가?'라는 질문을 던지게 된다.

    여기서 cost()메소드는 음료의 가격을 계산한 다음 슈퍼 클래스에서 구현한 cost()를 호출해서 첨가물 비용을 더한다.

     

    그런데 이렇게 구현하는 것에서 확신이 서지 않고, 만약에 첨가물 가경이 바뀌면 기존 코드를 모두 수정해야하는 것이 아닌지 의문이 남아있다.

     

    여기서 잠깐, 우리는 OCP 원칙을 살펴볼 수 있다.

    OCP 원칙

    OCP 원칙이란, '클래스는 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 한다'를 의미하는 디자인 원칙이다. 이 원칙은 우리가 새로운 기능을 추가할 때 급변하는 주변 환경에 잘 적응하는 유연하고 튼튼한 디자인을 만들 수 있게 도와준다.

     

    데코레이터 패턴

    앞서 본 것 처럼 상속을 써서 음료 가격과 첨가물(샷, 시럽, 우유, 휘핑크림 등)가격을 합해서 총 가격을 산출하는 방법은 클래스가 많아지고, 서브클래스는 적합하지 않은 기능을 추가해야하는 문제가 있었다.

     

    그러면 어떤 방법이 있을까?

    여기서 우리는 데코레이터 패턴을 사용할 수 있다.

     

    예를 들어, 모카와 휘핑크림을 추가한 다크 로스트 커피를 주문한다면 다음과 같이 장식할 수 있다.

    1. 다크 로스트 객체를 가져온다.

    2. 모카 객체로 장식한다.

    3. 휘핑크림 객체로 장식한다.

    4. cost() 메소드를 호출한다. 이때 첨가물의 가격을 계산하는 일은 각각의 객체에 위임한다.

     

    더 자세히 알아보면 다음과 같다.

    • DarkRoast 객체에서 시작한다.

    • 고객이 모카를 주문했으니까 Mocha 객체를 만들고 그 객체로 DarkRoast를 감싼다.

    여기서 모카 객체는 데코레이터이다. 객체의 형식은 객체가 장식하고 있는 객체를 반영하는데, 이 경우에는 Baverage를 반영하고 있다. 여기에서 반영한다는 것은, 같은 형식을 갖는다를 의미한다. 

    • 고객이 휘핑크림도 추가했으므로 Whip 데코레이터를 만들어 Mocha를 감싼다.

    Mocha와 Whip이 쌓여져 있는 DarkRoast는 여전히 Beverage 객체이기 때문에 cost()메소드 호출을 비롯한, DarkRoast와 관한 일이라면 무엇이든 할 수 있다.

    • 가격을 구할 떄는 가장 바깥 쪽에 있는 데코레이터인 Whip의 cost()를 호출하면 된다. 그러면 Whip은 그 객체가 장식하고 있는 객체에게 가격 계산을 위임한다. 가격이 구해지고 나면 휘핑크림의 가격을 더한 다음 그 결과 값을 리턴한다.

     

    다시 정리해보자면,

    • 데코레이터의 슈퍼클래스는 자신이 장식하고 있는 객체의 슈퍼클래스와 같다. 
    • 한 객체를 여러 개의 데코레이터로 감쌀 수 있다.
    • 데코레이터는 자신이 감싸고 있는 객체와 같은 슈퍼클래스를 가지고 있기에, 원래 객체가 들어갈 자리에 데코레이터 객체를 넣어도 상관이 없다.
    • 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 일 말고도 추가 작업을 수행할 수 있다.
    • 객체는 언제든지 감쌀 수 있으므로 실행 중에 필요한 데코레이터를 마음대로 적용할 수 있다.

    데코레이터 패턴의 정의

    데코레이터 패턴(Dacorator Pattern)으로 객체에 추가 요소를 동적으로 더할 수 있다. 데코레이터를 사용하면 서브 클래스를 만들 때보다 훨씬 유연하게 기능을 확장할 수 있다.

     

    정의보다 조금더 쉽게 이해하기 위해 클래스 다이어그램을 한번 살펴보자.

    그러면 이 방법으로 스타버즈 커피에 새로운 프레임워크를 적용하면 어떻게 될까?

    이 때, 우리는 데코레이터에서 클래스를 상속을 받는 행위가 아닌가?라는 생각을 하면서 이 패턴에 대한 의구심이 든다. 그런데 여기서 상속은 행동의 상속을 목표로 하다 보니 형식을 맞춘 것이다. 

     

    자세한 코드는 생략하겠습니다.

    데코레이터가 적용된 예

    우리가 실무중에 데코레이터 패턴을 어디서 볼 수 있을까? java.io 패키지에는 많은 클래스가 있다. 이제 데코레이터를 배운 우리는 I/O클래스가 왜 이렇게 되어있는지 이해할 수 있다. java.io클래스는 데코레이터 패턴으로 만들어졌다.

     

    파일에서 데이터를 읽어오는 스트림에  기능을 더하는 데코레이터를 사용하는 객체는 다음과 같은 형식으로 구성된다.

    BufferedInputStream과 ZipInputStream은 둘 다 FilterInputStream을 확장한 클래스이고 FilterInputStream은 InputStream을 확장한 클래스이다. InputStream은 추상 데코레이터 클래스 역할을 한다.

     

    다음은 팩토리 패턴으로 돌아오겠습니다!

Designed by Tistory.