Menu
Coddy logo textTech

데코레이터 패턴

Coddy Python 여정의 Object Oriented Programming 섹션에 포함된 레슨 — 64개 중 51번째.

데코레이터 패턴(Decorator Pattern)은 객체의 구조를 변경하지 않고 동적으로 새로운 기능을 추가합니다. 이 패턴은 객체를 감싸서 그 동작을 확장합니다.

다음은 간단한 커피 예시입니다:

class Coffee:
    def cost(self):
        return 5
    
    def description(self):
        return "Simple coffee"

커피에 기능을 추가하는 데코레이터를 생성합니다:

class MilkDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 2
    
    def description(self):
        return self.coffee.description() + " + Milk"

class SugarDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 1
    
    def description(self):
        return self.coffee.description() + " + Sugar"

각 데코레이터는 다른 객체를 감싸고 자신만의 기능을 추가합니다.

데코레이터를 사용하여 맞춤형 커피를 만드세요:

# 기본 커피로 시작
my_coffee = Coffee()
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

# 우유 추가
my_coffee = MilkDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

# 설탕 추가
my_coffee = SugarDecorator(my_coffee)
print(f"{my_coffee.description()}: ${my_coffee.cost()}")

텍스트 서식을 사용한 또 다른 예제를 만듭니다:

class Text:
    def __init__(self, content):
        self.content = content
    
    def render(self):
        return self.content

class BoldDecorator:
    def __init__(self, text):
        self.text = text
    
    def render(self):
        return f"<b>{self.text.render()}</b>"

class ItalicDecorator:
    def __init__(self, text):
        self.text = text
    
    def render(self):
        return f"<i>{self.text.render()}</i>"

# 단계별로 포맷된 텍스트 빌드
message = Text("Hello World")
message = BoldDecorator(message)
message = ItalicDecorator(message)

print(message.render())

출력:

Simple coffee: $5
Simple coffee + Milk: $7
Simple coffee + Milk + Sugar: $8
<i><b>Hello World</b></i>

핵심 포인트: 데코레이터 패턴(Decorator Pattern)은 원래 객체를 변경하지 않고 새로운 동작을 추가하기 위해 객체를 감쌉니다. 각 데코레이터는 감싸진 객체에 대한 참조를 유지하며 자신만의 기능을 추가합니다. 이를 통해 수많은 서브클래스를 생성하지 않고도 여러 데코레이터를 결합하여 유연한 기능 조합을 만들 수 있습니다.

challenge icon

챌린지

중급

이 챌린지에서는 데코레이터(Decorator) 디자인 패턴을 구현하여 커피숍을 위한 유연한 커피 주문 시스템을 만듭니다. 데코레이터 패턴을 사용하면 동일한 클래스의 다른 객체에 영향을 주지 않고 개별 객체에 동적으로 동작을 추가할 수 있습니다.

귀하는 다음과 같은 기능을 갖춘 커피숍 시스템을 구축하고 있습니다:

  • 다양한 종류의 음료 제공 (Espresso, Dark Roast, House Blend, Decaf)
  • 고객이 음료에 첨가물(Milk, Mocha, Soy, Whipped Cream)을 추가할 수 있도록 허용
  • 첨가물이 추가된 음료의 가격 계산
  1. beverage.py에서 추상 클래스 Beverage와 이를 상속받는 구체적인 클래스들을 완성하세요.
  2. condiment_decorator.py에서 CondimentDecorator 추상 클래스와 구체적인 데코레이터들을 구현하세요.
  3. driver.py에서 구현 내용을 검증하기 위한 테스트 시나리오를 구현하세요.

치트 시트

데코레이터 패턴(Decorator Pattern)은 객체를 래핑하여 동작을 확장함으로써, 객체의 구조를 변경하지 않고 동적으로 새로운 기능을 추가합니다.

커피 예제를 통한 기본 구조:

class Coffee:
    def cost(self):
        return 5
    
    def description(self):
        return "Simple coffee"

기능을 래핑하고 확장하는 데코레이터를 생성합니다:

class MilkDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 2
    
    def description(self):
        return self.coffee.description() + " + Milk"

class SugarDecorator:
    def __init__(self, coffee):
        self.coffee = coffee
    
    def cost(self):
        return self.coffee.cost() + 1
    
    def description(self):
        return self.coffee.description() + " + Sugar"

데코레이터를 체이닝하여 맞춤형 객체를 구성합니다:

# 기본 커피로 시작
my_coffee = Coffee()

# 데코레이터 추가
my_coffee = MilkDecorator(my_coffee)
my_coffee = SugarDecorator(my_coffee)

print(f"{my_coffee.description()}: ${my_coffee.cost()}")
# 출력: Simple coffee + Milk + Sugar: $8

텍스트 포맷팅 예제:

class Text:
    def __init__(self, content):
        self.content = content
    
    def render(self):
        return self.content

class BoldDecorator:
    def __init__(self, text):
        self.text = text
    
    def render(self):
        return f"<b>{self.text.render()}</b>"

class ItalicDecorator:
    def __init__(self, text):
        self.text = text
    
    def render(self):
        return f"<i>{self.text.render()}</i>"

# 데코레이터 체이닝
message = Text("Hello World")
message = BoldDecorator(message)
message = ItalicDecorator(message)

print(message.render())
# 출력: <i><b>Hello World</b></i>

주요 장점: 수많은 서브클래스를 생성하지 않고도 유연한 기능 조합이 가능하며, 래핑된 객체에 대한 참조를 유지하고 기능을 동적으로 추가할 수 있습니다.

직접 해보기

# 필요한 모든 클래스 가져오기
from beverage import Espresso, HouseBlend, DarkRoast, Decaf
from condiment_decorator import Milk, Mocha, Soy, Whip

# 종합 테스트 케이스 처리기
test_case = input()

if test_case == "basic_beverage_test":
    # 기본 음료 기능 테스트
    espresso = Espresso()
    print(f"Description: {espresso.get_description()}")
    print(f"Cost: ${espresso.cost():.2f}")

elif test_case == "all_beverages_test":
    # 모든 음료 유형 테스트
    beverages = [Espresso(), HouseBlend(), DarkRoast(), Decaf()]
    for beverage in beverages:
        print(f"{beverage.get_description()}: ${beverage.cost():.2f}")

elif test_case == "single_condiment_test":
    # 단일 첨가물 데코레이션 테스트
    espresso = Espresso()
    espresso_with_milk = Milk(espresso)
    print(f"Description: {espresso_with_milk.get_description()}")
    print(f"Cost: ${espresso_with_milk.cost():.2f}")

elif test_case == "multiple_condiments_test":
    # 여러 첨가물 데코레이션 테스트
    house_blend = HouseBlend()
    decorated_coffee = Mocha(Soy(Whip(house_blend)))
    print(f"Description: {decorated_coffee.get_description()}")
    print(f"Cost: ${decorated_coffee.cost():.2f}")

elif test_case == "all_condiments_test":
    # 한 음료에 모든 첨가물 테스트
    dark_roast = DarkRoast()
    fully_loaded = Milk(Mocha(Soy(Whip(dark_roast))))
    print(f"Description: {fully_loaded.get_description()}")
    print(f"Cost: ${fully_loaded.cost():.2f}")

elif test_case == "cost_calculation_test":
    # 다양한 조합에 대한 가격 계산 테스트
    espresso = Espresso()
    
    # 단일 추가
    with_milk = Milk(espresso)
    with_mocha = Mocha(espresso)
    
    print(f"Espresso: ${espresso.cost():.2f}")
    print(f"Espresso + Milk: ${with_milk.cost():.2f}")
    print(f"Espresso + Mocha: ${with_mocha.cost():.2f}")

elif test_case == "decoration_order_test":
    # 데코레이션 순서가 최종 결과에 영향을 미치지 않는지 테스트
    decaf = Decaf()
    
    order1 = Milk(Mocha(decaf))
    order2 = Mocha(Milk(decaf))
    
    print(f"Order 1 - {order1.get_description()}: ${order1.cost():.2f}")
    print(f"Order 2 - {order2.get_description()}: ${order2.cost():.2f}")
    print(f"Same cost: {order1.cost() == order2.cost()}")

elif test_case == "complex_order_test":
    # 복잡한 음료 주문 테스트
    orders = [
        Mocha(Whip(Espresso())),
        Soy(Milk(HouseBlend())),
        Whip(Mocha(Soy(DarkRoast()))),
        Milk(Decaf())
    ]
    
    for i, order in enumerate(orders, 1):
        print(f"Order {i}: {order.get_description()}")
        print(f"Cost: ${order.cost():.2f}")
        print("---")

elif test_case == "double_condiment_test":
    # 동일한 첨가물을 여러 번 추가하는 테스트
    espresso = Espresso()
    double_mocha = Mocha(Mocha(espresso))
    print(f"Description: {double_mocha.get_description()}")
    print(f"Cost: ${double_mocha.cost():.2f}")

elif test_case == "beverage_comparison_test":
    # 동일한 첨가물을 넣은 서로 다른 음료의 가격 비교
    base_beverages = [Espresso(), HouseBlend(), DarkRoast(), Decaf()]
    
    print("All beverages with Milk + Mocha:")
    for beverage in base_beverages:
        decorated = Milk(Mocha(beverage))
        print(f"{decorated.get_description()}: ${decorated.cost():.2f}")

elif test_case == "condiment_cost_test":
    # 개별 첨가물 가격 테스트
    espresso = Espresso()
    base_cost = espresso.cost()
    
    condiments = [
        ("Milk", Milk(espresso)),
        ("Mocha", Mocha(espresso)),
        ("Soy", Soy(espresso)),
        ("Whip", Whip(espresso))
    ]
    
    for name, decorated in condiments:
        additional_cost = decorated.cost() - base_cost
        print(f"{name} adds: ${additional_cost:.2f}")

elif test_case == "cheapest_most_expensive_test":
    # 가장 저렴한 베이스 음료와 가장 비싼 베이스 음료 찾기
    beverages = [
        ("Espresso", Espresso()),
        ("House Blend", HouseBlend()),
        ("Dark Roast", DarkRoast()),
        ("Decaf", Decaf())
    ]
    
    beverages.sort(key=lambda x: x[1].cost())
    print(f"Cheapest: {beverages[0][0]} - ${beverages[0][1].cost():.2f}")
    print(f"Most Expensive: {beverages[-1][0]} - ${beverages[-1][1].cost():.2f}")

elif test_case == "nested_decoration_test":
    # 깊게 중첩된 데코레이션 테스트
    beverage = HouseBlend()
    # 5단계 데코레이션 추가
    decorated = Whip(Soy(Mocha(Milk(Whip(beverage)))))
    print(f"Description: {decorated.get_description()}")
    print(f"Cost: ${decorated.cost():.2f}")
    
    # 첨가물 개수 세기
    description = decorated.get_description()
    condiment_count = description.count(" + ")
    print(f"Number of condiments: {condiment_count}")

elif test_case == "cost_breakdown_test":
    # 상세 가격 내역
    dark_roast = DarkRoast()
    print(f"Base Dark Roast: ${dark_roast.cost():.2f}")
    
    with_soy = Soy(dark_roast)
    print(f"+ Soy: ${with_soy.cost():.2f}")
    
    with_mocha = Mocha(with_soy)
    print(f"+ Mocha: ${with_mocha.cost():.2f}")
    
    with_whip = Whip(with_mocha)
    print(f"+ Whip: ${with_whip.cost():.2f}")
    
    final_cost = with_whip.cost()
    print(f"Final total: ${final_cost:.2f}")

elif test_case == "description_format_test":
    # 설명 형식의 일관성 테스트
    espresso = Espresso()
    test_combinations = [
        Milk(espresso),
        Mocha(Milk(espresso)),
        Whip(Mocha(Milk(espresso))),
        Soy(Whip(Mocha(Milk(espresso))))
    ]
    
    for combo in test_combinations:
        description = combo.get_description()
        print(f"Description: '{description}'")
        # 설명이 베이스 음료 이름으로 시작하는지 확인
        starts_correctly = description.startswith("Espresso")
        print(f"Starts with base name: {starts_correctly}")
        print("---")
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

Object Oriented Programming의 모든 레슨