디자인 패턴을 공부하다 보면 상속으로 기능을 계속 늘리는 방식이 금방 복잡해진다는 것을 만나게 됩니다.
기본 객체는 그대로 두고, 필요한 기능만 런타임에 하나씩 덧붙이고 싶을 때가 있습니다.
이때 사용할 수 있는 대표적인 구조가 Decorator Pattern입니다.
Decorator Pattern은 객체를 감싸는 wrapper를 여러 겹 쌓아, 기존 객체를 수정하지 않고 기능을 동적으로 확장하는 패턴입니다.
이번 글에서는 Decorator Pattern이 왜 필요한지, C++에서는 어떻게 구현할 수 있는지, 그리고 실무에서 어떤 상황에 적합한지 정리해보겠습니다.

1. Decorator Pattern은 무엇인가?
Decorator Pattern은 객체에 새로운 책임이나 기능을 동적으로 추가하기 위한 구조 패턴입니다.
기존 클래스에 직접 코드를 추가하거나, 기능 조합마다 하위 클래스를 새로 만들지 않고, 객체를 감싸는 decorator 객체를 사용합니다.
중요한 점은 decorator가 원래 객체와 같은 Component interface를 구현한다는 것입니다.
그래서 client 입장에서는 원래 객체인지 decorator로 감싼 객체인지 크게 구분하지 않고 사용할 수 있습니다.
| 구성 요소 | 역할 |
| Component | 원래 객체와 decorator가 함께 따르는 공통 interface |
| ConcreteComponent | 기본 기능을 제공하는 실제 객체 |
| Decorator | Component를 감싸고 같은 interface를 제공하는 base wrapper |
| ConcreteDecorator | 감싼 객체에 기능을 추가하는 실제 decorator |
Decorator Pattern의 핵심은 기능 확장을 상속 계층으로 폭발시키지 않고, 작은 wrapper 조합으로 표현하는 것입니다.
2. 왜 Decorator Pattern이 필요한가?
커피 주문을 예로 들어보겠습니다.
기본 커피에 우유, 휘핑, 샷 추가 같은 옵션을 붙일 수 있다고 가정해보겠습니다.
상속만으로 이 조합을 표현하면 클래스가 빠르게 늘어납니다.
- Espresso
- EspressoWithMilk
- EspressoWithWhip
- EspressoWithMilkAndWhip
- EspressoWithMilkAndShot
- EspressoWithMilkAndWhipAndShot
옵션이 3개일 때도 조합이 늘어나는데, 옵션이 10개 이상으로 커지면 상속 구조는 유지하기 어렵습니다.
Decorator Pattern은 이 문제를 조합으로 해결합니다.
기본 커피 객체를 만들고, 필요한 옵션 decorator로 하나씩 감싸면 됩니다.
3. 구조를 그림으로 이해하기
Decorator Pattern의 구조는 inheritance보다 composition을 중심으로 이해하는 것이 좋습니다.

Client는 Component interface만 바라봅니다.
ConcreteComponent는 기본 기능을 수행합니다.
Decorator는 내부에 Component를 보관하고 같은 interface를 구현합니다.
ConcreteDecorator는 내부 Component 호출 전후에 자신만의 기능을 추가합니다.
- Decorator Pattern 흐름
Client
|
v
Component interface
^
|
ConcreteDecorator C
|
ConcreteDecorator B
|
ConcreteDecorator A
|
ConcreteComponent
호출은 바깥 decorator에서 시작해 안쪽 Component로 delegate됩니다.
4. C++로 Decorator Pattern 구현하기
다음 예제는 커피에 옵션을 붙이는 Decorator Pattern 구현입니다.
Beverage가 Component 역할을 하고, Espresso가 ConcreteComponent 역할을 합니다.
BeverageDecorator는 공통 decorator base이고, MilkDecorator, WhipDecorator, ShotDecorator가 실제 기능을 추가합니다.
#include <iostream>
#include <memory>
#include <string>
#include <utility>
class Beverage {
public:
virtual ~Beverage() = default;
virtual std::string description() const = 0;
virtual int cost() const = 0;
};
class Espresso : public Beverage {
public:
std::string description() const override {
return "Espresso";
}
int cost() const override {
return 3000;
}
};
class BeverageDecorator : public Beverage {
public:
explicit BeverageDecorator(std::unique_ptr<Beverage> beverage)
: beverage_(std::move(beverage)) {}
protected:
const Beverage& beverage() const {
return *beverage_;
}
private:
std::unique_ptr<Beverage> beverage_;
};
class MilkDecorator : public BeverageDecorator {
public:
using BeverageDecorator::BeverageDecorator;
std::string description() const override {
return beverage().description() + " + milk";
}
int cost() const override {
return beverage().cost() + 500;
}
};
class WhipDecorator : public BeverageDecorator {
public:
using BeverageDecorator::BeverageDecorator;
std::string description() const override {
return beverage().description() + " + whip";
}
int cost() const override {
return beverage().cost() + 700;
}
};
class ShotDecorator : public BeverageDecorator {
public:
using BeverageDecorator::BeverageDecorator;
std::string description() const override {
return beverage().description() + " + extra shot";
}
int cost() const override {
return beverage().cost() + 800;
}
};
int main() {
std::unique_ptr<Beverage> drink = std::make_unique<Espresso>();
drink = std::make_unique<MilkDecorator>(std::move(drink));
drink = std::make_unique<WhipDecorator>(std::move(drink));
drink = std::make_unique<ShotDecorator>(std::move(drink));
std::cout << drink->description() << '\n';
std::cout << "cost: " << drink->cost() << '\n';
}
실행 결과
Espresso + milk + whip + extra shot
cost: 5000
각 decorator는 자신이 감싸고 있는 Beverage에 작업을 delegate한 뒤, description과 cost에 자신의 기능을 덧붙입니다.
Client는 최종 객체를 Beverage로만 다루므로, 몇 개의 decorator가 감싸져 있는지 몰라도 사용할 수 있습니다.
5. 코드에서 봐야 할 핵심
이 예제에서 가장 중요한 부분은 decorator도 Beverage를 상속한다는 점입니다.
Decorator가 Component와 같은 interface를 제공하기 때문에, decorator를 다시 다른 decorator로 감쌀 수 있습니다.
즉, 객체 하나가 다음 객체의 입력이 되고, 그 결과가 다시 다음 decorator의 입력이 됩니다.
| 코드 요소 | 패턴에서의 의미 |
| Beverage | Component interface |
| Espresso | ConcreteComponent |
| BeverageDecorator | 공통 Decorator |
| MilkDecorator, WhipDecorator, ShotDecorator | ConcreteDecorator |
| std::unique_ptr<Beverage> | 감싼 객체의 ownership을 명확히 표현 |
C++에서는 ownership이 중요합니다.
위 예제는 decorator가 내부 Beverage를 소유하므로 std::unique_ptr을 사용했습니다.
외부에서 객체 생명주기를 관리해야 하는 구조라면 reference, raw pointer, shared_ptr 중 무엇이 맞는지 별도로 판단해야 합니다.
6. 상속만 사용할 때와 무엇이 다른가?
상속은 컴파일 시점에 클래스 구조를 고정합니다.
Decorator Pattern은 런타임에 객체를 조합합니다.
이 차이는 기능 조합이 많아질수록 중요해집니다.
| 구분 | 상속 중심 설계 | Decorator Pattern |
| 기능 추가 | 새 하위 클래스 추가 | 새 decorator 추가 |
| 기능 조합 | 조합마다 클래스가 늘어날 수 있음 | 객체를 여러 겹 감싸서 조합 |
| 런타임 변경 | 어려움 | 객체 조합을 바꾸면 가능 |
| 구조 이해 | 클래스 계층을 보면 비교적 명확함 | 실제 wrapping 순서를 추적해야 함 |
Decorator Pattern은 기능 조합을 유연하게 만들지만, 객체가 여러 겹으로 감싸지면 호출 흐름을 추적하기 어려워질 수 있습니다.
따라서 무조건 적용하기보다 기능 조합이 실제로 자주 바뀌는지 먼저 확인해야 합니다.
7. 자주 하는 오해
오해 1. Decorator Pattern은 단순 상속보다 항상 좋다
기능 조합이 거의 없고 클래스 수가 적다면 상속이나 단순 composition이 더 읽기 쉬울 수 있습니다.
Decorator Pattern은 기능 조합이 많고 확장 지점이 명확할 때 장점이 커집니다.
오해 2. Decorator는 원래 객체를 반드시 바꿔야 한다
Decorator는 원래 객체를 수정하지 않고 감싸는 방식입니다.
기능 추가는 wrapper에서 이루어지고, 원래 객체는 자신의 기본 책임에 집중합니다.
오해 3. Decorator와 Proxy는 같은 패턴이다
둘 다 객체를 감싸는 구조를 가질 수 있지만 의도가 다릅니다.
Decorator는 기능을 추가하는 것이 목적이고, Proxy는 접근 제어, lazy loading, remote access처럼 대상 객체 접근을 제어하는 것이 목적입니다.
오해 4. Decorator 순서는 중요하지 않다
Decorator가 호출 전후에 동작을 추가한다면 순서가 결과에 영향을 줄 수 있습니다.
Logging, caching, authorization 같은 기능은 어떤 wrapper가 바깥에 있는지에 따라 의미가 달라질 수 있습니다.
8. 실무 관점
실무에서는 Decorator Pattern을 기능 조합이 실제 요구사항인 곳에 제한적으로 쓰는 것이 좋습니다.
예를 들어 stream 처리, middleware chain, logging wrapper, caching wrapper, UI component extension처럼 기본 객체에 부가 기능을 단계적으로 붙이는 구조에 잘 맞습니다.
반대로 기능이 단순히 하나만 추가되거나 조합이 거의 없다면 별도 클래스를 여러 개 만드는 비용이 더 클 수 있습니다.
C++에서는 특히 ownership과 객체 생명주기를 먼저 정해야 합니다.
decorator가 내부 객체를 소유하는지, 외부 객체를 참조만 하는지에 따라 unique_ptr, shared_ptr, reference 선택이 달라집니다.
9. 면접 질문 예시
9-1. Decorator Pattern은 어떤 문제를 해결하나요?
기존 객체를 수정하지 않고 기능을 동적으로 추가해야 하는 문제를 해결합니다.
상속으로 모든 기능 조합을 만들면 클래스 수가 늘어나기 쉬운데, Decorator Pattern은 wrapper 조합으로 기능을 확장합니다.
9-2. Decorator Pattern과 상속의 차이는 무엇인가요?
상속은 클래스 구조를 통해 기능을 확장하고, Decorator Pattern은 객체를 감싸는 composition으로 기능을 확장합니다.
Decorator Pattern은 런타임 조합이 가능하다는 점에서 기능 조합이 많은 상황에 유리합니다.
9-3. C++에서 Decorator Pattern을 구현할 때 주의할 점은 무엇인가요?
가장 중요한 것은 ownership과 lifetime입니다.
decorator가 내부 Component를 소유한다면 unique_ptr이 적합할 수 있고, 소유하지 않는다면 reference나 pointer의 lifetime을 명확히 관리해야 합니다.
10. 정리
- Decorator Pattern은 객체를 wrapper로 감싸 기능을 동적으로 추가하는 구조 패턴입니다.
- 핵심은 Component와 Decorator가 같은 interface를 제공한다는 점입니다.
- 상속 조합이 폭발하는 상황에서는 decorator 조합이 더 유연할 수 있습니다.
- C++에서는 wrapper가 내부 객체를 소유하는지 먼저 결정해야 합니다.
- 기능 조합이 많지 않다면 단순 상속이나 composition이 더 나을 수 있습니다.
Decorator Pattern은 기능을 덧붙이는 방식 자체보다, 객체의 기본 책임과 부가 기능을 분리하는 사고방식이 중요합니다.
'소프트웨어 디자인 패턴' 카테고리의 다른 글
| 옵저버 패턴 쉽게 이해하기 (0) | 2026.06.20 |
|---|---|
| 전략 패턴 쉽게 이해하기 (0) | 2026.06.20 |