Ch.3 데코레이터 패턴

상속맨, 디자인에 눈을 뜨다

About

(가상의) 초대형 커피 전문점 스타버즈의 주문 시스템을 예제로 알아본다.

음료를 나타내는 추상 클래스 Beverage 는 인스턴스 변수인 description (및 getDescription()), 그리고 추상 메소드인 cost() 를 갖고 있다. 그 밑에 서브 클래스인 HouseBlend, DarkRoast, Decaf, Espresso에서 cost() 함수를 구현해야 한다.

  • 지금은 4종류라서 괜찮지만, 커피를 마실 때 다양한 옵션(milk, soy, mocha, whip, ...)을 추가하면서 클래스의 조합이 말 그대로 '폭발'하게 된다.

The Code

OCP (Open-Closed Principle)

클래스는 확장에는 열려 있어야 하지만 변경에는 닫혀 있어야 한다.

  • 상속 대신 합성으로 객체의 행동을 확장하면 실행 중에 동적으로 행동을 설정할 수 있고, 여러 기능을 새로 추가할 수 있다. 객체를 동적으로 구성하면 기존 코드를 고치는 대신 새로운 코드를 만들어 기능을 추가할 수 있다. 기존 코드는 건드리지 않으니 코드 수정에 따른 버그, 의도하지 않은 부작용을 막을 수 있다.

  • OCP를 적용하는 방법 중 데코레이터 패턴을 배워보자.

  • 무조건 OCP를 적용한다면 괜히 시간을 낭비할 수 있고, 필요 이상으로 복잡하고 이해하기 어려운 코드를 낳을 수 있으니 주의하자.

Decorator Pattern으로 장식해보자

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

  1. DarkRoast 객체를 가져온다.

  2. Mocha 객체로 장식한다.

  3. Whip 객체로 장식한다.

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

여기서 어떻게 객체를 '장식'할까? 힌트는 데코레이터 객체를 '래퍼' 객체라고 생각해보는 것이다.

가격 계산은 바깥쪽의 데코레이터인 Whip부터 cost()를 호출하고, 그 객체가 장식하고 있는 객체인 Mochacost(), 그리고 그 객체가 장식하고 있는 DarkRoast 객체의 cost() 메소드를 호출하도록 가격 계산을 위임한다. 최종적으로 그 결과값에 휘핑크림의 가격까지를 더해 반환하게 된다.

데코레이터 패턴은...

  • 데코레이터의 슈퍼 클래스 == 자신이 장식하고 있는 객체의 슈퍼클래스

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

  • 데코레이터는 감싸고 있는 객체와 같은 슈퍼클래스를 갖고 있기에 원래 객체가 들어갈 자리를 데코레이터가 대신할 수 있다.

  • (KEY) 데코레이터는 자신이 장식하고 있는 객체에게 어떤 행동을 위임하는 것 말고도 추가 작업을 수행할 수 있다.

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

데코레이터 패턴을 만들기 위해 상속을 사용하게 되는데, 행동을 물려받으려는 상속이 아니라 형식을 맞추기 위해 상속을 사용한다. (물론 인터페이스를 사용할 수도 있다.) 기능을 구현하기 위해서 합성을 사용할 수 있으니 유연한 방식이라고 할 수 있다.

주의사항

  • 데코레이터로 감싸면 원래 객체가 무엇인지를 모르기 때문에 특별 할인 등의 작업을 하긴 어려우무로 다시 생각해봐야 한다.

  • 데코레이터 패턴을 쓰면 관리할 객체가 늘어나서 코딩할 때 실수할 가능성도 높아지는데, 실제로는 팩토리나 빌더 같은 패턴으로 데코레이터를 만들고 사용하기 때문에 캡슐화가 잘 된다.

  • 데코레이터가 같은 객체를 감싸고 있는 다른 데코레이터를 알기 위해선(데코레이터 패턴의 의도와 어긋나지만), 넘겨 받은 해당 속성을 잘 파싱해서 하는 수 밖에 없다.

데코레이터 패턴의 예시

  • Java I/O API (java.io.*)

    • ( ZipInputStream ( BufferedInputStream ( FileInputStream ) ) )

  • 보다보면 알겠지만 데코레이터 패턴을 사용해서 디자인하다 보면 잡다한 클래스가 너무 많아져서, 사용하는 개발자는 이해하기 어렵다는 단점이 있다.

  • 다만 데코레이터가 어떻게 작동하는지 이해하면 다른 사람이 데코레이터 패턴을 활용해서 만든 API를 끌어 쓰더라도 클래스를 데코레이터로 감싸서 원하는 행동을 구현할 수 있다.

  • 초기화에 들어가는 코드가 복잡해지는 단점이 있으나, 추후에 나오는 팩토리와 빌더 패턴으로 보완할 수 있다.

책의 예제에 있는 FilterInputStream의 데코레이터를 만드는 예제는 생략한다.

Summary

  • 데코레이터 패턴은 객체에 기능을 유연하게 추가하기 알맞은 방법이다.

  • 조합의 폭발을 막아주고, 기능이나 동작을 동적으로 바꿀 수 있어 유연하게 사용할 수 있다.

  • 단 사용하는 입장에서 코드가 이해하기 어려워지고, 클래스를 어떻게 사용하는지 잘 알고 있어야 한다. 초기화에도 코드를 많이 작성해야 한다. 이는 팩토리와 빌더 패턴으로 보완할 수 있다.

My Thoughts

  • 데코레이터 패턴은 실제로 사용처를 많이 본 패턴인 것 같다.

  • 단점도 존재하지만 실제 많은 곳에 도입된 만큼 장점도 많은 패턴이라고 생각한다. 적절한 때에 사용하면 좋겠다.

Last updated