Architecture & Design Pattern 전체 링크
참고
데코레이터 (Decorator Pattern) - 구조 패턴
- 기존 객체의 동작을 수정하지 않고, 그 객체의 기능을 확장하거나 수정하는 패턴
- 런타임에 동적으로 객체의 기능을 추가하거나 수정할 수 있다.
구현
- Component : 데코레이터와 구체적인 컴포넌트를 동일한 타입으로 처리하는 추상 클래스 / 인터페이스
- ConcreteComponent : 기본 동작을 구현하는 컴포넌트
- Decorator : 컴포넌트를 상속하고, 동적으로 기능을 추가, 변경하는 메서드를 가지는 추상 클래스 / 인터페이스
- ConcreteDecorator : 실제로 기능을 추가하거나 변경하는 데코레이터
장점
- SRP(단일 책임 원칙), 클래스를 단일 기능 단위로 분리하여 유지보수와 확장성을 높인다.
- OCP(개방-폐쇄 원칙), 객체에 새로운 기능을 추가/수정할 때, 기존 코드를 건드리지 않는다.
- 자식 클래스를 만들지 않고 객체의 기능을 확장할 수 있다.
- 데코레이터를 조합(Wrapping)하여 다양한 기능을 구성할 수 있다. (재사용성)
단점
- 많은 수의 데코레이터가 쌓이면 코드가 복잡해진다.
- 런타임에 동적으로 기능을 추가하기 때문에 오버헤드가 발생할 수 있다.
- 특정 데코레이터(or Wrapper)를 제거하기 어렵다.
맥도날드
맥도날드에서는 다양한 햄버거를 판매한다.
햄버거 추상 클래스를 상속하여 빅백이나 맥모닝 등 여러 햄버거를 판매하고 있다.
빅맥이나 맥모닝은 여러 특징에 따라 가격이 정해져서 price() 메서드를 구현해야 한다.
그런데 고객들은 같은 햄버거라도 토마토, 패티, 양파와 같은 재료가 더 많이 추가된 햄버거를 원할 수 있다.
햄버거 클래스만 상속받아서 위의 모든 요구 사항을 구현하면 아래와 같이 자식 클래스가 매우 많아진다.
즉, 상속을 통해서 클래스를 확장해도 유연성이 좋지 않다.
위와 같은 상황을 방지하기 위해 Hamburger 클래스에 재료를 변수로 추가해 보자.
이제 price를 구현할 때, 각 재료의 has/set 메서드를 활용하여 가격을 결정지을 수 있다.
int price()
{
int p = 0;
if (hasTomato()) p += tomatoPrice;
if (hasOnion()) p += onionPrice;
if (hasPatty()) p += pattyPrice;
...
}
위의 방식으로 클래스를 고쳐도 아래와 같은 문제가 발생한다.
먼저 재료가 변경될 때마다 기존 코드를 수정해야 하고, 종류가 추가될 때마다 Hamburger 클래스를 수정해야 한다.
또한 새로운 햄버거에는 기존의 재료가 들어가지 않는 경우에도 기존 재료 메서드를 상속받는다.
그리고 현재 메서드는 토마토나 양파를 2개 이상 추가할 수 없다.
따라서 위의 방법은 개방-폐쇄 원칙 (OCP, Open-Closed Principle)에 어긋난다.
→ 클래스는 확장에 대해서는 열려 있어야 하고, 코드 변경에 대해서는 닫혀 있어야 한다.
데코레이터 패턴 (Decorator Pattern)
ConcreteComponent에서 새로운 행동(재료)을 동적으로 추가하게 된다.
Decorator 안에는 Component 객체가 들어있다.
각 재료(ConcreteDecorator)들은 Component를 확장할 수 있다.
데코레이터 패턴을 이용해 햄버거에 대한 클래스 다이어그램을 그려보면 다음과 같다.
이제 실제로 구현을 해보자.
요구 사항은 다음과 같다.
빅맥의 기본 가격은 6000원, 맥모닝은 4000원이다.
토마토, 양파, 패티를 추가할 때마다 각각 2000원 / 1000원 / 3000원의 비용이 추가된다.
Component인 햄버거 추상 클래스는 다음과 같다.
class Hamburger
{
public:
Hamburger() = default;
virtual ~Hamburger() = default;
virtual string getName() const { return name; }
virtual int price() = 0;
protected:
string name = "Abstract Hamburger";
};
구체적인 햄버거(Concrete Component)는 햄버거를 상속받아 이름과 가격을 정한다.
class BigMac : public Hamburger
{
public:
BigMac() { name = "BigMac"; }
int price() override { return 6000; }
};
재료를 추가하는 데코레이터에는 Wrapping된 객체(햄버거)를 참조하기 위한 변수가 필요하다.
class IngredientDecorator : public Hamburger
{
public:
IngredientDecorator() = default;
IngredientDecorator(unique_ptr<Hamburger> h) : hamburger(move(h)) {}
virtual string getName() const override { return "Unknown Ingredient"; }
protected:
unique_ptr<Hamburger> hamburger = nullptr;
};
구체적인 재료(ConcreteDecorator)는 가격을 추가하도록 메서드를 구현한다.
그리고 햄버거의 이름에 어떤 재료가 추가되었는지 알 수 있도록 이름을 변경한다.
class Tomato : public IngredientDecorator
{
public:
Tomato() = default;
Tomato(unique_ptr<Hamburger> b) : IngredientDecorator(move(b)) {}
string getName() const override { return hamburger->getName() + " +Tomato"; }
int price() override { return hamburger->price() + 2000; }
};
햄버거에 재료를 추가하는 방법은 다음과 같다.
unique_ptr<Hamburger> hamburger1 = make_unique<BigMac>();
hamburger1 = make_unique<Tomato>(move(hamburger1)); // 토마토 추가
hamburger1 = make_unique<Patty>(move(hamburger1)); // 패티 추가
현재 햄버거 정보를 출력하는 메서드를 만들어서 확인할 수 있다.
void printHamburger(const unique_ptr<Hamburger>& h)
{
cout << h->getName() << " / " << h->price() << endl;
}
전체 코드는 다음과 같다.
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class Hamburger
{
public:
Hamburger() = default;
virtual ~Hamburger() = default;
virtual string getName() const { return name; }
virtual int price() = 0;
protected:
string name = "Abstract Hamburger";
};
class IngredientDecorator : public Hamburger
{
public:
IngredientDecorator() = default;
IngredientDecorator(unique_ptr<Hamburger> h) : hamburger(move(h)) {}
virtual string getName() const override { return "Unknown Ingredient"; }
protected:
unique_ptr<Hamburger> hamburger = nullptr;
};
class BigMac : public Hamburger
{
public:
BigMac() { name = "BigMac"; }
int price() override { return 6000; }
};
class MacMorning : public Hamburger
{
public:
MacMorning() { name = "MacMorning"; }
int price() override { return 4000; }
};
/* ---------------------- Ingredient ---------------------- */
class Tomato : public IngredientDecorator
{
public:
Tomato() = default;
Tomato(unique_ptr<Hamburger> b) : IngredientDecorator(move(b)) {}
string getName() const override { return hamburger->getName() + " +Tomato"; }
int price() override { return hamburger->price() + 2000; }
};
class Onion : public IngredientDecorator
{
public:
Onion() = default;
Onion(unique_ptr<Hamburger> b) : IngredientDecorator(move(b)) {}
string getName() const override { return hamburger->getName() + " +Onion"; }
int price() override { return hamburger->price() + 1000; }
};
class Patty : public IngredientDecorator
{
public:
Patty() = default;
Patty(unique_ptr<Hamburger> b) : IngredientDecorator(move(b)) {}
string getName() const override { return hamburger->getName() + " +Patty"; }
int price() override { return hamburger->price() + 3000; }
};
void printHamburger(const unique_ptr<Hamburger>& h)
{
cout << h->getName() << " / " << h->price() << endl;
}
int main(void)
{
unique_ptr<Hamburger> hamburger1 = make_unique<BigMac>();
printHamburger(hamburger1);
hamburger1 = make_unique<Tomato>(move(hamburger1));
printHamburger(hamburger1);
hamburger1 = make_unique<Patty>(move(hamburger1));
printHamburger(hamburger1);
hamburger1 = make_unique<Patty>(move(hamburger1));
printHamburger(hamburger1);
hamburger1 = make_unique<Onion>(move(hamburger1));
printHamburger(hamburger1);
/* ----------------------------------------------------------- */
unique_ptr<Hamburger> hamburger2 = make_unique<MacMorning>();
printHamburger(hamburger2);
hamburger2 = make_unique<Tomato>(move(hamburger2));
printHamburger(hamburger2);
hamburger2 = make_unique<Tomato>(move(hamburger2));
printHamburger(hamburger2);
hamburger2 = make_unique<Patty>(move(hamburger2));
printHamburger(hamburger2);
hamburger2 = make_unique<Onion>(move(hamburger2));
printHamburger(hamburger2);
return 0;
}
햄버거 크기 변경
이제 고객의 다양한 요구사항에 따라 햄버거에 크기가 도입이 되었다.
햄버거의 크기마다 재료의 크기도 다르기 때문에 재료의 가격이 크기에 따라 달라진다.
(ex. 토마토는 Size에 따라 2000원 / 1000원 / 500원 추가)
class Hamburger
{
public:
enum class Size { LARGE, MEDIUM, SMALL };
Hamburger() = default;
virtual ~Hamburger() = default;
virtual string getName() const { return name; }
virtual int price() = 0;
virtual void setSize(Size s) { size = s; }
virtual Size getSize() const { return size; }
virtual string getSizeString() const
{
string result;
switch (size)
{
case Size::LARGE: return "LARGE ";
case Size::MEDIUM: return "MEDIUM";
case Size::SMALL: return "SMALL ";
}
return "UNKNOWN";
}
protected:
string name = "Abstract Hamburger";
Size size = Size::LARGE;
};
재료 데코레이터의 경우 햄버거의 메서드를 그대로 사용하도록 한다.
class IngredientDecorator : public Hamburger
{
public:
IngredientDecorator() = default;
IngredientDecorator(unique_ptr<Hamburger> h) : hamburger(move(h)) {}
virtual string getName() const override { return "Unknown Ingredient"; }
void setSize(Size s) override { hamburger->setSize(s); }
Size getSize() const override { return hamburger->getSize(); }
string getSizeString() const override { return hamburger->getSizeString(); }
protected:
unique_ptr<Hamburger> hamburger = nullptr;
};
각 재료(ConcreteDecorator)에는 Size에 따라 price가 변경되도록 수정하면 된다.
class Tomato : public IngredientDecorator
{
public:
Tomato() = default;
Tomato(unique_ptr<Hamburger> b) : IngredientDecorator(move(b)) {}
string getName() const override { return hamburger->getName() + " +Tomato"; }
int price() override
{
int p = hamburger->price();
switch (hamburger->getSize())
{
case Size::LARGE: return p += 2000;
case Size::MEDIUM: return p += 1000;
case Size::SMALL: return p += 500;
}
return p;
}
};
햄버거의 정보를 출력하는 메서드에 크기에 대한 정보도 보여주자.
void printHamburger(const unique_ptr<Hamburger>& h)
{
cout << "[" << h->getSizeString() << "] " << h->getName() << " / " << h->price() << endl;
}
햄버거의 크기를 변경했을 때, 정상적인 가격이 나오는지 확인해 보자.
#include <iostream>
#include <string>
#include <memory>
using namespace std;
class Hamburger
{
public:
enum class Size { LARGE, MEDIUM, SMALL };
Hamburger() = default;
virtual ~Hamburger() = default;
virtual string getName() const { return name; }
virtual int price() = 0;
virtual void setSize(Size s) { size = s; }
virtual Size getSize() const { return size; }
virtual string getSizeString() const
{
string result;
switch (size)
{
case Size::LARGE: return "LARGE ";
case Size::MEDIUM: return "MEDIUM";
case Size::SMALL: return "SMALL ";
}
return "UNKNOWN";
}
protected:
string name = "Abstract Hamburger";
Size size = Size::LARGE;
};
class IngredientDecorator : public Hamburger
{
public:
IngredientDecorator() = default;
IngredientDecorator(unique_ptr<Hamburger> h) : hamburger(move(h)) {}
virtual string getName() const override { return "Unknown Ingredient"; }
void setSize(Size s) override { hamburger->setSize(s); }
Size getSize() const override { return hamburger->getSize(); }
string getSizeString() const override { return hamburger->getSizeString(); }
protected:
unique_ptr<Hamburger> hamburger = nullptr;
};
class BigMac : public Hamburger
{
public:
BigMac() { name = "BigMac"; }
int price() override { return 6000; }
};
class MacMorning : public Hamburger
{
public:
MacMorning() { name = "MacMorning"; }
int price() override { return 4000; }
};
/* ---------------------- Ingredient ---------------------- */
class Tomato : public IngredientDecorator
{
public:
Tomato() = default;
Tomato(unique_ptr<Hamburger> b) : IngredientDecorator(move(b)) {}
string getName() const override { return hamburger->getName() + " +Tomato"; }
int price() override
{
int p = hamburger->price();
switch (hamburger->getSize())
{
case Size::LARGE: return p += 2000;
case Size::MEDIUM: return p += 1000;
case Size::SMALL: return p += 500;
}
return p;
}
};
class Onion : public IngredientDecorator
{
public:
Onion() = default;
Onion(unique_ptr<Hamburger> b) : IngredientDecorator(move(b)) {}
string getName() const override { return hamburger->getName() + " +Onion"; }
int price() override
{
int p = hamburger->price();
switch (hamburger->getSize())
{
case Size::LARGE: return p += 1000;
case Size::MEDIUM: return p += 500;
case Size::SMALL: return p += 100;
}
return p;
}
};
class Patty : public IngredientDecorator
{
public:
Patty() = default;
Patty(unique_ptr<Hamburger> b) : IngredientDecorator(move(b)) {}
string getName() const override { return hamburger->getName() + " +Patty"; }
int price() override
{
int p = hamburger->price();
switch (hamburger->getSize())
{
case Size::LARGE: return p += 3000;
case Size::MEDIUM: return p += 2000;
case Size::SMALL: return p += 1000;
}
return p;
}
};
void printHamburger(const unique_ptr<Hamburger>& h)
{
cout << "[" << h->getSizeString() << "] " << h->getName() << " / " << h->price() << endl;
}
int main(void)
{
unique_ptr<Hamburger> hamburger1 = make_unique<BigMac>();
printHamburger(hamburger1);
hamburger1 = make_unique<Tomato>(move(hamburger1));
printHamburger(hamburger1);
hamburger1 = make_unique<Patty>(move(hamburger1));
printHamburger(hamburger1);
hamburger1 = make_unique<Patty>(move(hamburger1));
printHamburger(hamburger1);
hamburger1 = make_unique<Onion>(move(hamburger1));
printHamburger(hamburger1);
hamburger1->setSize(Hamburger::Size::MEDIUM);
printHamburger(hamburger1);
hamburger1->setSize(Hamburger::Size::SMALL);
printHamburger(hamburger1);
/* ----------------------------------------------------------- */
unique_ptr<Hamburger> hamburger2 = make_unique<MacMorning>();
printHamburger(hamburger2);
hamburger2 = make_unique<Tomato>(move(hamburger2));
printHamburger(hamburger2);
hamburger2 = make_unique<Tomato>(move(hamburger2));
printHamburger(hamburger2);
hamburger2 = make_unique<Patty>(move(hamburger2));
printHamburger(hamburger2);
hamburger2 = make_unique<Onion>(move(hamburger2));
printHamburger(hamburger2);
hamburger2->setSize(Hamburger::Size::MEDIUM);
printHamburger(hamburger2);
hamburger2->setSize(Hamburger::Size::SMALL);
printHamburger(hamburger2);
return 0;
}
동적 데코레이터 (Dynamic Decorator)
위의 햄버거는 런타임에 햄버거의 조합을 만들기 때문에 동적 데코레이터다.
동적 데코레이터의 또 다른 예시로 색상(밝기)과 채도를 추가할 수 있는 Light를 예로 들어보자.
#include <iostream>
#include <string>
using namespace std;
class Light
{
public:
virtual void turnOn() = 0;
};
class Lamp : public Light
{
public:
Lamp() = default;
Lamp(int b) : brightness(b) {}
void setBrightness(int b) { brightness = b; }
void turnOn() override { cout << "Light On, brightness : " << brightness << endl; }
private:
int brightness;
};
class ColorLight : public Light
{
public:
ColorLight(Light* l, const string& c) : light(l), color(c) {}
void turnOn() override
{
light->turnOn();
cout << "+ color : " << color << endl;
}
private:
Light* light;
string color;
};
class SaturatedLight : public Light
{
public:
SaturatedLight(Light* l, const int saturation) : light(l), saturation{ saturation } {}
void turnOn() override
{
light->turnOn();
cout << "+ saturation : " << saturation << endl;
}
private:
Light* light;
int saturation;
};
int main(void)
{
Lamp* lamp = new Lamp(5);
lamp->turnOn();
lamp->setBrightness(10);
lamp->turnOn();
cout << endl;
ColorLight* blueLamp = new ColorLight(lamp, "blue");
blueLamp->turnOn();
// blueLamp->setBrightness(20); // compile error
cout << endl;
SaturatedLight* clearBlueLamp = new SaturatedLight(blueLamp, 100);
clearBlueLamp->turnOn();
//clearBlueLamp->setBrightness(20); // compile error
return 0;
}
Lamp 클래스에서 파란색을 추가하고, 채도를 100으로 설정하였다.
하지만 동적 데코레이터는 같은 데코레이터를 계속해서 추가할 수 있는 문제점이 발생한다.
예를 들어 blueLamp에 다시 "red"를 추가하면 파랗고 빨간 램프가 된다.
ColorLight* redBlueLamp = new ColorLight(blueLamp, "red");
redBlueLamp->turnOn();
또한 ColorLight가 Lamp를 상속받은 것은 아니므로, Lamp의 setBrightness 메서드를 사용할 수 없다.
blueLamp->setBrightness(20); // compile error
정적 데코레이터 (Static Decorator)
데코레이터와 데코레이션 된 객체의 모든 함수(setBrightness)에 접근하고 싶다면, 정적 데코레이터를 만들면 된다.
템플릿으로 클래스를 상속(Mixin Inheritance)를 사용하면 정적 데코레이터를 만들 수 있다.
여기서는 가변 템플릿 매개변수(template <typename...Args>)를 이용하여 생성자를 만들었다.
template <typename T>
class ColorLight : public T
{
public:
template <typename...Args>
ColorLight(const string& color, Args ...args)
: T(forward<Args>(args)...), color{ color } {}
void turnOn() override
{
T::turnOn();
cout << "+ color : " << color << endl;
}
private:
string color;
};
이제 아래와 같이 클래스를 정의할 수 있고, 관련 클래스의 모든 메서드를 사용할 수 있다.
SaturatedLight<ColorLight<Lamp>>* clearBlueLamp
= new SaturatedLight<ColorLight<Lamp>>(100, "blue", 5);
clearBlueLamp->setBrightness(10);
clearBlueLamp->turnOn();
상속 제한하기
만약 Light와 비슷한 인터페이스를 가지고 있는 TV가 있다고 가정하자.
class TV
{
public:
virtual void turnOn() = 0;
virtual void turnOff() = 0;
};
class SmartTV : public TV
{
public:
SmartTV() = default;
SmartTV(int p) : price(p) {}
void turnOn() override { cout << "TV is on" << endl; }
void turnOff() override { cout << "TV is off" << endl; }
private:
int price;
};
TV에 색상과 채도를 추가하는 것은 이상하지만, 정적 데코레이터를 만드는데 문법적으로 문제가 없다.
ColorLight<SaturatedLight<SmartTV>>* redBlurTV
= new ColorLight<SaturatedLight<SmartTV>>("red", 1, 5);
redBlurTV->turnOn();
위와 같은 경우를 방지하기 위해서 클래스에 static_assert를 추가한다.
is_base_of를 이용해 T가 Light인 경우만 허용하도록 하자.
template <typename T>
class ColorLight : public T
{
static_assert(is_base_of<Light, T>::value, "You must provide Light as a template argument.");
public:
...
};
template <typename T>
class SaturatedLight : public T
{
static_assert(is_base_of<Light, T>::value, "You must provide Light as a template argument.");
public:
...
};
이제 TV에 색상과 채도 데코레이터를 추가하면 컴파일 에러가 발생한다.
전체 코드는 다음과 같다.
#include <iostream>
#include <string>
using namespace std;
class TV
{
public:
virtual void turnOn() = 0;
virtual void turnOff() = 0;
};
class SmartTV : public TV
{
public:
SmartTV() = default;
SmartTV(int p) : price(p) {}
void turnOn() override { cout << "TV is on" << endl; }
void turnOff() override { cout << "TV is off" << endl; }
private:
int price;
};
/* ------------------------------------------------------------ */
class Light
{
public:
virtual void turnOn() = 0;
};
class Lamp : public Light
{
public:
Lamp() = default;
Lamp(int b) : brightness(b) {}
void setBrightness(int b) { brightness = b; }
void turnOn() override { cout << "Light On, brightness : " << brightness << endl; }
private:
int brightness;
};
template <typename T>
class ColorLight : public T
{
static_assert(is_base_of<Light, T>::value, "You must provide Light as a template argument.");
public:
template <typename...Args>
ColorLight(const string& color, Args ...args)
: T(forward<Args>(args)...), color{ color } {}
void turnOn() override
{
T::turnOn();
cout << "+ color : " << color << endl;
}
private:
string color;
};
template <typename T>
class SaturatedLight : public T
{
static_assert(is_base_of<Light, T>::value, "You must provide Light as a template argument.");
public:
template<typename...Args>
SaturatedLight(const int saturation, Args ...args)
: T(forward<Args>(args)...), saturation{ saturation } {}
void turnOn() override
{
T::turnOn();
cout << "+ saturation : " << saturation << endl;
}
private:
int saturation;
};
int main(void)
{
SaturatedLight<ColorLight<Lamp>>* clearBlueLamp
= new SaturatedLight<ColorLight<Lamp>>(100, "blue", 5);
clearBlueLamp->setBrightness(10);
clearBlueLamp->turnOn();
cout << endl;
/*
ColorLight<SaturatedLight<SmartTV>>* redBlurTV
= new ColorLight<SaturatedLight<SmartTV>>("red", 1, 5);
redBlurTV->turnOn();
*/
ColorLight<SaturatedLight<Lamp>>* redBlurLamp
= new ColorLight<SaturatedLight<Lamp>>("red", 1, 5);
redBlurLamp->turnOn();
return 0;
}
함수형 데코레이터 (Functional Decorator)
함수에 대해서도 다른 행동을 추가할 수 있다.
만약 어떤 메서드에 앞, 뒤로 로그를 남기고 싶다면 아래와 같이 코드를 작성하면 된다.
#include <iostream>
#include <string>
using namespace std;
template<typename T>
void decoratorLogger(T method)
{
cout << "Decorator: Before calling the original function" << endl;
method();
cout << "Decorator: After calling the original function" << endl;
}
void myOperation()
{
cout << "Test Decorator" << endl;
}
int main(void)
{
decoratorLogger(myOperation);
return 0;
}
ClassDiagram.md
```mermaid
classDiagram
class Hamburger {
name
getName()
price()
}
class BigMac {
price()
}
class MacMorning {
price()
}
class IngredientDecorator {
Hamburger hamburger
getName()
}
class Tomato {
getName()
price()
}
class Onion {
getName()
price()
}
class Patty {
getName()
price()
}
Hamburger <|-- BigMac
Hamburger <|-- MacMorning
Hamburger <|-- IngredientDecorator
IngredientDecorator <|-- Tomato
IngredientDecorator <|-- Onion
IngredientDecorator <|-- Patty
```
'개발 > Architecture & Design Pattern' 카테고리의 다른 글
C++ - 개방-폐쇄 원칙 (OCP, Open-Closed Principle) (0) | 2024.02.12 |
---|---|
C++ - 의존 역전 원칙 (DIP, Dependency Inversion Principle) (0) | 2024.02.12 |
C++ - 복합체, 컴포지트 패턴 (Composite Pattern) (1) | 2024.02.09 |
C++ - 반복자, 이터레이터 패턴 (Iterator Pattern) (0) | 2024.02.04 |
C++ - 옵저버 패턴 (Observer Pattern) (1) | 2024.01.30 |
댓글