데코레이터(Decorator) 패턴The Decorator Pattern
데코레이터 패턴을 통해 객체에 동적으로 기능을 추가하는 방법How to add features to objects dynamically with the decorator pattern
데코레이터(Decorator)
decorate : 장식하다, 꾸미다
데코레이터 패턴은 단어의 뜻처럼 객체를 꾸며주는 패턴입니다. “객체에 동적으로 새로운 책임을 추가할 수 있다.” 라고 많이 이야기 합니다. 음… 정확히 어떤 의미인지 이해가 가지 않습니다.

여러가지 재료를 넣은 커피
여러가지 커피를 클래스로 구현해보며 데코레이터 패턴 대해 알아봅시다!
상황
커피는 들어가는 재료에 따라 다양한 커피를 만들수 있습니다. 커피에 우유를 넣으면 ‘라떼’, 커피에 초코릿 가루를 넣으면 ‘카페모카’ 등등… 무수히 많은 조합으로 커피를 만들수 있습니다.

코드
우선은 상속을 통해서 간단하게 구현이 가능할 것 같습니다. 그럼 기본이 되는 Coffee 클래스를 상속해서 각 재료가 추가된 클래스를 만들어서 각 커피를 구현합니다.
class Coffee:
def __init__(self):
self._cost = 4000
self._recipe = ["Coffee"]
def cost(self) -> int:
return self._cost
def recipe(self) -> list[str]:
return self._recipe
class LatteCoffee(Coffee):
def __init__(self):
super().__init__()
self._cost += 500
self._recipe += ["Milk"]
class MochaCoffee(Coffee):
def __init__(self):
super().__init__()
self._cost += 1000
self._recipe += ["Mocha"]
아직 까지는 상속을 이용해서 각 커피를 만드는게 어려움이 없습니다. 하지만 커피를 만들 수 있는 방법이 다양해지면 어떨까요?
if __name__ == "__main__":
coffee = Coffee()
latte_coffee = LatteCoffee()
mocha_coffee = MochaCoffee()
print(f"coffee / {coffee.recipe()} / {coffee.cost()}")
print(f"Latte coffee / {latte_coffee.recipe()} / {latte_coffee.cost()}")
print(f"Mocha coffee / {mocha_coffee.recipe()} / {mocha_coffee.cost()}")
coffee / ['Coffee'] / 4000
Latte coffee / ['Coffee', 'Milk'] / 4500
Mocha coffee / ['Coffee', 'Mocha'] / 5000
너무 많은 커피 조합
상황
커피를 만들다보니 넣을수 있는 재료가 점점 늘어났습니다. 재료가 늘어남에 따라서 조합해서 먹을 수 있는 커피의 종류도 감당하기 어려울 만큼 늘었습니다.

코드
기존에는 상속을 통해 각 커피 클래스를 구현했습니다. 하지만 상속을 사용해서 많은 경우의 수를 모두 구현하기는 점점 힘들어 집니다. 상속에 상속을 계속하는게 맞는지 의구심이 마구마구 들기 시작합니다.
class MilkMochaCoffee(LatteCoffee):
def __init__(self):
super().__init__()
self._cost += 1000
self._recipe += ["Mocha"]
class MilkMochaVanillaCoffee(MilkMochaCoffee):
def __init__(self):
super().__init__()
self._cost += 500
self._recipe += ["Vanilla"]
데코레이터 패턴의 특징
이제 데코레이터 패턴의 특징 두 가지를 다시 기억해 봅시다.
- 동적으로
- 새로운 책임을 추가
앞서 상속을 이용하는 방식을 생각해 봅시다. 상속을 이용한 경우, 코드를 실행 하기 전에 이미 클래스로 각 커피 객체를 미리 전부 만들어두고 사용해야 한다는 단점이 있었습니다.
데코레이터 패턴을 사용하면 이러한 문제를 해결 할 수 있습니다. 미리 클래스를 만들 필요없이, 동적으로 객체에 필요한 기능을 덧붙여 나갈 수 있게 만들어 줍니다.
데코레이터를 이용한 커피 조합
코드
Beverage는 기본 컴포넌트를 정의하는 추상 클래스입니다. cost()와 recipe() 메서드를 추상 메서드로 정의하여 모든 음료가 이를 구현하도록 강제합니다.
from abc import ABC, abstractmethod
class Beverage(ABC):
@abstractmethod
def cost(self) -> int:
pass
@abstractmethod
def recipe(self) -> list[str]:
pass
Coffee 클래스는 Beverage의 구체적인 구현체입니다. cost()와 recipe() 메서드를 오버라이드하여 구현합니다.
from typing import override
class Coffee(Beverage):
@override
def cost(self) -> int:
return 5000
@override
def recipe(self) -> list[str]:
return ["Coffee"]
Decorator 클래스는 데코레이터 패턴의 가장 중요한 클래스입니다.
이 클래스는 Beverage를 상속받으면서(inheritance), 동시에 Beverage 객체를 내부적으로 구성(composition)하고 있습니다.
class Decorator(Beverage):
def __init__(self, beverage: Beverage):
self.beverage = beverage
def cost(self) -> int:
return self.beverage.cost()
def recipe(self) -> list[str]:
return self.beverage.recipe()
Decorator를 상속받아 기본 기능을 유지하면서 추가 기능을 더합니다. cost()와 recipe() 메서드를 오버라이드하여 기존 음료에 모카나 우유를 추가하는 기능을 구현합니다.
class Mocha(Decorator):
def cost(self) -> int:
return super().cost() + 1000
def recipe(self)-> list[str]:
return super().recipe() + ["Mocha"]
class Milk(Decorator):
def cost(self) -> int:
return super().cost() + 500
def recipe(self) -> list[str]:
return super().recipe() + ["Milk"]
상속을 이용해서 다양한 커피를 만들려고 했던 걸 기억해 봅시다. 상속으로 Mocha를 한번 넣은 커피와 두번 넣은 클래스를 각각 만들고, 거기다 우유를 추가한 클래스를 또 상속으로 만들고…
이 문제를 데코레이터 패턴을 사용함으로써 객체에 새로운 책임을 동적으로 추가 할 수 있습니다.
if __name__ == '__main__':
beverage: Beverage = Coffee()
a = Mocha(beverage)
b = Mocha(a)
c = Milk(b)
print(f"Decorator Coffee / {c.recipe()} / {c.cost()}")
Coffee / ['Coffee', 'Mocha', 'Mocha', 'Milk'] / 7500
클래스 다이어그램
Decorator 클래스가 Beverage 추상 클래스를 상속받으면서 동시에 내부에 Beverage 객체를 구성하는 형태를 기억합시다.
%%{
init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#2a3844',
'lineColor': '#fff',
'primaryTextColor': '#fff',
'tertiaryColor': '#fff'
}
}
}%%
classDiagram
direction LR
class Beverage {
<<interface>>
cost()* int
recipe()* list[str]
}
class Coffee {
cost() int
recipe() list[str]
}
Decorator *-- Beverage
Decorator ..|> Beverage
Beverage <|.. Coffee
class Decorator {
Beverage beverage
cost()* int
recipe()* list[str]
}
class Mocha {
cost() int
recipe() list[str]
}
class Milk {
cost() int
recipe() list[str]
}
Mocha --|> Decorator
Milk --|> Decorator
마무리
상속을 통한 클래스 확장이 비효율적인 경우에, 데코레이터 패턴을 이용하면 동적으로 기능을 추가할 수 있습니다. 구현에서 눈여겨 볼 부분은 Decorator 클래스가 상속과 동시에 객체를 구성하는 형태입니다. io 처리나, 알람 메시지를 조합하는 경우에 데코레이터 패턴을 활용 할 수 있습니다. 😊
# 기본 알림
notifier = BaseNotifier()
notifier.send("message!")
# SMS와 Slack으로 알림
notifier = SlackNotifier(SMSNotifier(BaseNotifier()))
notifier.send("Important message!")
# SMS, Slack, Jira로 알림
notifier = JiraNotifier(SlackNotifier(SMSNotifier(BaseNotifier())))
notifier.send("Critical issue detected!")
Decorator
decorate : to adorn, to embellish
As the word suggests, the decorator pattern is a pattern that decorates objects. You’ll often hear it described as “attaching new responsibilities to an object dynamically.” Hmm… it’s not exactly clear what that means.

Coffee with Lots of Ingredients
Let’s learn about the decorator pattern by implementing various coffees as classes!
The Situation
You can make all kinds of coffee depending on what goes into it. Add milk to coffee and you get a ‘latte’; add chocolate powder and you get a ‘caffè mocha’, and so on… There are countless combinations.

The Code
At first, it seems like we could implement this simply with inheritance. We inherit from a base Coffee class and create a class for each added ingredient to implement each coffee.
class Coffee:
def __init__(self):
self._cost = 4000
self._recipe = ["Coffee"]
def cost(self) -> int:
return self._cost
def recipe(self) -> list[str]:
return self._recipe
class LatteCoffee(Coffee):
def __init__(self):
super().__init__()
self._cost += 500
self._recipe += ["Milk"]
class MochaCoffee(Coffee):
def __init__(self):
super().__init__()
self._cost += 1000
self._recipe += ["Mocha"]
So far, making each coffee through inheritance isn’t hard. But what happens when the ways to make coffee multiply?
if __name__ == "__main__":
coffee = Coffee()
latte_coffee = LatteCoffee()
mocha_coffee = MochaCoffee()
print(f"coffee / {coffee.recipe()} / {coffee.cost()}")
print(f"Latte coffee / {latte_coffee.recipe()} / {latte_coffee.cost()}")
print(f"Mocha coffee / {mocha_coffee.recipe()} / {mocha_coffee.cost()}")
coffee / ['Coffee'] / 4000
Latte coffee / ['Coffee', 'Milk'] / 4500
Mocha coffee / ['Coffee', 'Mocha'] / 5000
Too Many Coffee Combinations
The Situation
As we kept making coffee, the list of ingredients kept growing. And as the ingredients grew, the number of coffee combinations grew beyond what we could handle.

The Code
Until now, we implemented each coffee class through inheritance. But covering every possible combination with inheritance gets harder and harder. You start to seriously doubt whether stacking inheritance on top of inheritance is the right call.
class MilkMochaCoffee(LatteCoffee):
def __init__(self):
super().__init__()
self._cost += 1000
self._recipe += ["Mocha"]
class MilkMochaVanillaCoffee(MilkMochaCoffee):
def __init__(self):
super().__init__()
self._cost += 500
self._recipe += ["Vanilla"]
Characteristics of the Decorator Pattern
Let’s recall the two characteristics of the decorator pattern.
- Dynamically
- Adding new responsibilities
Think back to the inheritance approach. Its drawback was that every coffee had to be defined in advance as a class before the code ever ran.
The decorator pattern solves this problem. There’s no need to create classes ahead of time; it lets you attach the features an object needs dynamically, one by one.
Coffee Combinations with Decorators
The Code
Beverage is an abstract class that defines the base component. It declares cost() and recipe() as abstract methods, forcing every beverage to implement them.
from abc import ABC, abstractmethod
class Beverage(ABC):
@abstractmethod
def cost(self) -> int:
pass
@abstractmethod
def recipe(self) -> list[str]:
pass
The Coffee class is a concrete implementation of Beverage. It overrides the cost() and recipe() methods.
from typing import override
class Coffee(Beverage):
@override
def cost(self) -> int:
return 5000
@override
def recipe(self) -> list[str]:
return ["Coffee"]
The Decorator class is the most important class in the decorator pattern.
This class inherits from Beverage (inheritance) while also holding a Beverage object internally (composition).
class Decorator(Beverage):
def __init__(self, beverage: Beverage):
self.beverage = beverage
def cost(self) -> int:
return self.beverage.cost()
def recipe(self) -> list[str]:
return self.beverage.recipe()
These classes inherit from Decorator, keeping its basic behavior while adding more. They override cost() and recipe() to add mocha or milk to an existing beverage.
class Mocha(Decorator):
def cost(self) -> int:
return super().cost() + 1000
def recipe(self)-> list[str]:
return super().recipe() + ["Mocha"]
class Milk(Decorator):
def cost(self) -> int:
return super().cost() + 500
def recipe(self) -> list[str]:
return super().recipe() + ["Milk"]
Remember how we tried to make various coffees with inheritance? One class for coffee with one shot of mocha, another for two shots, then yet another inherited class with milk added…
With the decorator pattern, we can solve this by adding new responsibilities to an object dynamically.
if __name__ == '__main__':
beverage: Beverage = Coffee()
a = Mocha(beverage)
b = Mocha(a)
c = Milk(b)
print(f"Decorator Coffee / {c.recipe()} / {c.cost()}")
Coffee / ['Coffee', 'Mocha', 'Mocha', 'Milk'] / 7500
Class Diagram
Keep in mind the shape here: the Decorator class inherits from the abstract Beverage class while also holding a Beverage object inside.
%%{
init: {
'theme': 'base',
'themeVariables': {
'primaryColor': '#2a3844',
'lineColor': '#fff',
'primaryTextColor': '#fff',
'tertiaryColor': '#fff'
}
}
}%%
classDiagram
direction LR
class Beverage {
<<interface>>
cost()* int
recipe()* list[str]
}
class Coffee {
cost() int
recipe() list[str]
}
Decorator *-- Beverage
Decorator ..|> Beverage
Beverage <|.. Coffee
class Decorator {
Beverage beverage
cost()* int
recipe()* list[str]
}
class Mocha {
cost() int
recipe() list[str]
}
class Milk {
cost() int
recipe() list[str]
}
Mocha --|> Decorator
Milk --|> Decorator
Wrapping Up
When extending classes through inheritance becomes inefficient, the decorator pattern lets you add features dynamically. The part of the implementation worth noting is how the Decorator class both inherits and composes an object at the same time. You can put the decorator pattern to use for things like I/O handling or combining notification messages. 😊
# Basic notification
notifier = BaseNotifier()
notifier.send("message!")
# Notify via SMS and Slack
notifier = SlackNotifier(SMSNotifier(BaseNotifier()))
notifier.send("Important message!")
# Notify via SMS, Slack, and Jira
notifier = JiraNotifier(SlackNotifier(SMSNotifier(BaseNotifier())))
notifier.send("Critical issue detected!")