1. Decorator 패턴이란

1.1 Decorator 패턴의 정의

주어진 상황 및 용도에 따라 어떤 객체에 책임을 덧붙이는 패턴


정의만읽어서는 이게 무슨 내용인지 알기가 어렵다. UML과 함께 보도록 하자.


1.2 UML



Component - 최상위 클래스, 혹은 인터페이스, 혹은 추상 클래스.

ConcreteComponent - Component 인터페이스를 구현한 구상(Concrete) 클래스.

Decorator - Component를 상속한 클래스들을 장식 하는 기능에 대한 인터페이스.

ConcreteDecoratorA,ConcreteDecoratorB - Decorator 인터페이스를 구현한 구상 클래스


ConcreteDecoratorA, ConcreteDecoratorB는 Component를 상속받는 모든 종류의 클래스를 장식하기 위해 사용된다. 그래서 ConcreteComponent 뿐만 아니라 Decorator 클래스들도 장식 할 수 있다.



2. 적용 예제


Head First Desgin Pattern에 나오는 예제이다.


스타버즈 커피샵은 클래스를 이용하여 판매하는 음료들을 관리한다. 스타버즈 커피에서 판매하는 음료들은 다음과 같은 클래스 구조로 구성되었다.




Beverage라는 인터페이스는 가격을 계산하기 위한 cost 값과, 음료의 설명을 위한 description 값을 가진다. 자세한 가격 계산 및 description에 관한 것은 Beverage 인터페이스를 구현한 구상 클래스들의 몫이다.


그런데 여기서 커피를 주문 할 때 스팀 우유, 두유, 모카(초콜릿), 휘핑 크림 등의 첨가물을 추가했을때, 첨가물을 포함한 음료의 총 가격을 계산할 수 있는 기능이 필요해졌다.

이에따라 우리는 이 클래스 구조를 4가지 종류의 첨가물(스팀우유, 두유, 모카, 휘핑크림)을 포함한 음료의 총 가격을 계산 할 수 있도록 수정해야한다.

첨가물을 이 시스템에 추가할 방법을 찾아야 하는데, 어떤 방법이 있을까?


여기에 Decorator 패턴을 적용해 보자.



CondimentDecorator는 음료에 추가 될 수 있는 첨가물 데코레이터를 나타내는 인터페이스이며, SteamdMilk, Mocha 등의 클래스는 이 첨가물 인터페이스를 구현한다.

여기서 눈여겨 볼 점은 CondimentDecorator가 Beverage를 구성요소로 가진다는 점이다. CondimentDecorator가 구성요소로 가진 Beverage가 첨가물이 더해질 음료가 된다.

가령, Mocha 객체가 beverage로 HouseBlended를 가지면 이 음료는 모카 하우스 블렌디드 커피가 된다.

그리고 첨가물은 첨가물이 더해진 음료도 장식 할 수 있다. 그래서 다음과 같은 구조도 가능하다.



이 음료의 이름은 라떼 더블모카 하우스 블렌디드 커피가 될 것이다.

그리고 우리의 목표였던 가격 계산은 다음과 같이 수행 할 수 있다.



다음은 최상위 인터페이스인 Beverage의 구현 코드이다.

#pragma once #include using namespace std; //The base class for beverages class Beverage { public: virtual float getCost() = 0; virtual const string& getDescription() = 0; protected: float cost; //$ notation string description; };

Beverage 인터페이스는 음료의 가격을 계산하는 getCost() 메소드와 getDescription() 메소드를 갖는다.


다음은 Beverage 인터페이스를 구현한 HouseBlended 클래스다.

<HouserBlended.h>

#pragma once #include "Beverage.h" //House Blended Coffee class HouseBlended : public Beverage { public: HouseBlended(); const string& getDescription() override; float getCost() override; };

<HouserBlended.cpp> #include "HouseBlended.h" HouseBlended::HouseBlended() { this->cost = 0.89; this->description = "House Blended Coffee"; } const string& HouseBlended::getDescription() { return description; } float HouseBlended::getCost() { return cost; }

음료의 getCost(), getDescription 메소드의 구현은 단순히 클래스 내에 cost값과 description 값을 리턴하도록 구현한다.


다음은 Decorator 패턴을 위해 구현한 CondimentDecorator 클래스다.

#pragma once #include "../Beverage.h" //Decorator interface for Beverage class CondimentDecorator : public Beverage { protected: Beverage * beverage; string cattedString; //string for returning the concatenated string. //it's not a good way to return new object in c++, because it's not deleted automatically. };

장식할 음료에 접근하기 위해 Beverage* beverage; 필드를 갖는다.

cattedString은 getDescription() 메소드 구현을 위해서, 두개의 string 객체를 합쳐서 리턴해야하는데, 리턴할 스트링 값을 담기 위해 사용된다.

return string1 + string2; 와같이 리턴하면, 합쳐진 결과 string 객체는 지역변수처럼 함수가 종료되면서 메모리 공간이 해제되어 사라져버린다. 그래서 필드 값으로 리턴 할 값을 담을 공간을 만들어 주었다.


다음은 Decorator 인터페이스를 구현한 구상 클래스인 Mocha 클래스를 보자. 

<Mocha .h>

#include "CondimentDecorator.h" //Mocha condiment class Mocha : public CondimentDecorator { public: Mocha(Beverage* const beverage); float getCost() override; const string& getDescription() override; };


<Mocha .cpp>

#include "Mocha.h" Mocha::Mocha(Beverage* const beverage) { this->beverage = beverage; this->cost = 0.2; this->description = "Mocha, "; } float Mocha::getCost() { return cost + beverage->getCost(); } const string& Mocha::getDescription() { cattedString = description + beverage->getDescription(); return cattedString; }

Mocha 클래스는 가격을 계산할 때, 자신의 가격 + beverage의 가격을 더해서 리턴한다.

getDescription() 메소드도 유사하게 구현되었고, CondimentDecorator를 구현한 모든 구상 클래스가 동일한 구조다.



구현한 프로젝트는 다음 깃허브 페이지에서 확인 할 수 있다. https://github.com/InvincibleTyphoon/DecoratorPattern

책에서는 Java를 사용하지만, 개인적으로 C++을 더 선호하는 관계로, 구현은 C++로 했다.


3. Decorator 패턴의 장점

- 구성(Composition)을 통해 객체의 행동을 확장하기 때문에 실행중에 동적으로 행동을 설정 할 수 있다. (객체지향 설계 원칙 - 상속보다는 구성을 이용. Composition over inheritance.)


- 기존 코드를 건드리지 않고, 확장을 통해 새로운 행동을 추가 할 수 있다.(OCP(Open-Closed Principle) 원칙을 준수)


둘 다 객체지향 설계 원칙과 관련되는 이야기다.


상속이 객체지향 설계에서 강력한 힘을 발휘하긴 하지만, 일반적으로 상속보다 구성을 우선시 해야한다. 두 클래스의 관계에 있어서 상속 관계보다는 구성 관계가 더 느슨하기 때문에, 구성이 상속에 비해 변화에 더 유연하게 대처할 수 있다.


기존 코드를 수정하면 아무래도 새로운 클래스를 작성하는 것 보다 오류를 범할 위험성이 있다. 수정해야 할 코드에 의존하는 클래스가 많다면 그 코드를 수정하는 것이 각각의 클래스에 어떤 영향을 미칠지 다 고려해야한다. 

하지만 새로운 클래스를 만드는 것은 새로 만든 클래스에 오류가 없는지만 고려하면 된다.


4. 주의 사항

- 데코레이터 패턴은 구성요소의 형식을 알아내어 그 형식을 바탕으로 처리하는 작업에는 부적합하다.

가령, 위의 예제에서 하우스 블렌디드 커피만 특별 할인을 해주는 상황은 처리 하기는 어렵다.


- 데코레이터 패턴을 적용하면 관리해야할 객체가 늘어난다.

데코레이터 패턴을 적용하면, 데코레이터 클래스들이 새로 생겨나기 마련이다. 그에 따라 코딩하는 과정에서 실수가 늘어날 가능성이 있다.


5. 레퍼런스

- Head First Design Pattern(O'REILLY media)

- 위키피디아 데코레이터 패턴 : https://en.wikipedia.org/wiki/Decorator_pattern

- 위키피디아 Composition over inheritance : https://en.wikipedia.org/wiki/Composition_over_inheritance#Benefits

- 위키피디아 개방-폐쇄원칙 : https://ko.wikipedia.org/wiki/개방-폐쇄_원칙


블로그 이미지

서기리보이

,