디자인 패턴 - 데코레이터(Decorator)

데코레이터(Decorator) 패턴에 대해서 알아보겠습니다.

written by tiaz0128

데코레이터(Decorator)


decorate : 장식하다, 꾸미다

데코레이터 패턴은 단어의 뜻처럼 객체를 꾸며주는 패턴입니다. “객체에 동적으로 새로운 책임을 추가할 수 있다.” 라고 많이 이야기 합니다. 음… 정확히 어떤 의미인지 이해가 가지 않습니다.

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"]

데코레이터 패턴의 특징


이제 데코레이터 패턴의 특징 두 가지를 다시 기억해 봅시다.

  1. 동적으로
  2. 새로운 책임을 추가

앞서 상속을 이용하는 방식을 생각해 봅시다. 상속을 이용한 경우, 코드를 실행 하기 전에 이미 클래스로 각 커피 객체를 미리 전부 만들어두고 사용해야 한다는 단점이 있었습니다.

데코레이터 패턴을 사용하면 이러한 문제를 해결 할 수 있습니다. 미리 클래스를 만들 필요없이, 동적으로 객체에 필요한 기능을 덧붙여 나갈 수 있게 만들어 줍니다.

데코레이터를 이용한 커피 조합


코드

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!")
python 디자인패턴 데코레이터

tiaz0128

Eat Sleep Coding.

Never Never Giveup.

💫 Python  |  BackEnd  |  Cloud Architect  |  DevOps