Menu
Coddy logo textTech

데코레이터 패턴

Coddy C++ 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 104개 중 98번째.

데코레이터 패턴(Decorator pattern)은 객체들을 데코레이터라고 불리는 특별한 객체들로 감싸서 동적으로 새로운 동작들을 추가할 수 있게 해줍니다. 컴파일 타임에 동작을 추가하는 상속과 달리, 데코레이터는 원래의 클래스를 수정하지 않고도 런타임에 기능을 확장할 수 있게 해줍니다.

이 패턴은 원본 객체와 데코레이터가 모두 동일한 인터페이스를 구현하도록 함으로써 작동합니다. 각 데코레이터는 컴포넌트에 대한 참조를 유지하며, 래핑된 객체에 위임하기 전이나 후에 자신만의 동작을 추가합니다:

#include <iostream>
#include <memory>

// 컴포넌트 인터페이스
class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
    virtual ~Coffee() = default;
};

// 구체적인 컴포넌트
class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override { return "Coffee"; }
    double getCost() const override { return 2.0; }
};

// 기본 데코레이터
class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};

// 구체적인 데코레이터들
class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Milk";
    }
    double getCost() const override { return coffee->getCost() + 0.5; }
};

class SugarDecorator : public CoffeeDecorator {
public:
    SugarDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Sugar";
    }
    double getCost() const override { return coffee->getCost() + 0.2; }
};

데코레이터는 중첩될 수 있습니다. 각 데코레이터는 이전 데코레이터를 감싸며, 기능을 계층별로 쌓아 올립니다:

int main() {
    std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
    order = std::make_unique<MilkDecorator>(std::move(order));
    order = std::make_unique<SugarDecorator>(std::move(order));
    
    std::cout << order->getDescription() << ": $" << order->getCost();
    // 출력: Coffee, Milk, Sugar: $2.7
}

다른 객체에 영향을 주지 않고 객체에 동적으로 책임을 추가해야 할 때, 또는 서브클래싱을 통해 기능을 확장하는 것이 클래스 수의 폭발적인 증가를 초래할 때 데코레이터(Decorator)를 사용하세요.

challenge icon

챌린지

쉬움

데코레이터(Decorator) 패턴을 사용하여 피자 주문 시스템을 구축해 봅시다. 고객이 기본 피자에서 시작하여 다양한 토핑을 추가할 수 있는 유연한 시스템을 만들게 됩니다. 각 토핑은 이전 주문을 감싸며 고유한 비용과 설명을 추가합니다. 이는 데코레이터 패턴의 완벽한 활용 사례입니다. 토핑은 수많은 서브클래스를 만들지 않고도 어떤 조합으로든 쌓을 수 있습니다.

코드는 다음 세 개의 파일로 구성됩니다:

  • Pizza.h: 컴포넌트 인터페이스와 기본 피자 클래스를 정의합니다.

    순수 가상 메서드인 getDescription()(문자열 반환)과 getCost()(double 반환), 그리고 가상 소멸자를 가진 추상 클래스 Pizza를 생성하세요.

    그런 다음 기본 피자를 나타내는 구체 클래스 PlainPizza를 생성하세요. 이 클래스는 설명으로 "Pizza"를 반환하고 기본 비용으로 8.0을 가져야 합니다.

  • ToppingDecorator.h: 데코레이터 베이스 클래스와 구체적인 토핑 데코레이터들을 구축합니다.

    Pizza를 상속받고 래핑된 컴포넌트에 대한 std::unique_ptr<Pizza>를 보유하는 ToppingDecorator 클래스를 생성하세요. 이것은 모든 토핑 데코레이터의 베이스 역할을 합니다.

    세 가지 구체적인 토핑 데코레이터를 구현하세요:

    • CheeseTopping — 설명에 ", Cheese"를 추가하고 비용에 1.5를 추가합니다.
    • PepperoniTopping — 설명에 ", Pepperoni"를 추가하고 비용에 2.0을 추가합니다.
    • MushroomTopping — 설명에 ", Mushrooms"를 추가하고 비용에 1.0을 추가합니다.

    각 데코레이터는 래핑된 피자에게 작업을 위임한 다음 자신의 기여분을 추가해야 합니다.

  • main.cpp: 사용자 입력을 기반으로 맞춤형 피자 주문을 생성합니다.

    세 개의 입력(각각 yes 또는 no)을 읽습니다:

    1. 치즈를 추가하시겠습니까?
    2. 페퍼로니를 추가하시겠습니까?
    3. 버섯을 추가하시겠습니까?

    PlainPizza로 시작하여 입력에 따라 적절한 토핑 데코레이터로 감싸세요 (순서: 치즈, 페퍼로니, 버섯). 마지막으로 주문 세부 정보를 다음 형식으로 출력하세요:

    Order: [description]
    Total: $[cost]

예를 들어, 입력이 yes, yes, no인 경우:

Order: Pizza, Cheese, Pepperoni
Total: $11.5

입력이 yes, no, yes인 경우:

Order: Pizza, Cheese, Mushrooms
Total: $10.5

입력이 no, no, no인 경우:

Order: Pizza
Total: $8

입력이 yes, yes, yes인 경우:

Order: Pizza, Cheese, Pepperoni, Mushrooms
Total: $12.5

각 데코레이터가 이전 데코레이터를 어떻게 감싸며 설명과 비용을 계층별로 쌓아가는지 확인해 보세요. 기존 피자나 토핑 코드를 수정하지 않고도 새로운 데코레이터 클래스를 생성하여 새로운 토핑을 쉽게 추가할 수 있습니다. 이것이 바로 데코레이터 패턴의 장점입니다.

치트 시트

데코레이터 패턴(Decorator pattern)은 객체를 데코레이터라고 불리는 특별한 객체로 감싸서 동적으로 새로운 행동을 추가할 수 있게 해줍니다. 컴파일 타임에 행동을 추가하는 상속과 달리, 데코레이터는 원래 클래스를 수정하지 않고도 런타임에 기능을 확장할 수 있게 해줍니다.

이 패턴은 원래 객체와 데코레이터가 모두 동일한 인터페이스를 구현하도록 함으로써 작동합니다. 각 데코레이터는 컴포넌트에 대한 참조를 유지하며, 감싸진 객체에 작업을 위임하기 전이나 후에 자신만의 행동을 추가합니다.

구조

컴포넌트 인터페이스(Component interface): 구체적인 컴포넌트와 데코레이터 모두를 위한 공통 인터페이스를 정의합니다.

class Coffee {
public:
    virtual std::string getDescription() const = 0;
    virtual double getCost() const = 0;
    virtual ~Coffee() = default;
};

구체적인 컴포넌트(Concrete component): 데코레이션될 수 있는 기본 객체입니다.

class SimpleCoffee : public Coffee {
public:
    std::string getDescription() const override { return "Coffee"; }
    double getCost() const override { return 2.0; }
};

기본 데코레이터(Base decorator): 컴포넌트에 대한 참조를 유지하고 동일한 인터페이스를 구현합니다.

class CoffeeDecorator : public Coffee {
protected:
    std::unique_ptr<Coffee> coffee;
public:
    CoffeeDecorator(std::unique_ptr<Coffee> c) : coffee(std::move(c)) {}
};

구체적인 데코레이터(Concrete decorators): 감싸진 객체에 작업을 위임하고 자신만의 기능을 추가함으로써 특정 행동을 더합니다.

class MilkDecorator : public CoffeeDecorator {
public:
    MilkDecorator(std::unique_ptr<Coffee> c) : CoffeeDecorator(std::move(c)) {}
    std::string getDescription() const override {
        return coffee->getDescription() + ", Milk";
    }
    double getCost() const override { return coffee->getCost() + 0.5; }
};

데코레이터 쌓기

데코레이터는 쌓일 수 있습니다. 각 데코레이터는 이전 데코레이터를 감싸며 층층이 기능을 쌓아 올립니다:

std::unique_ptr<Coffee> order = std::make_unique<SimpleCoffee>();
order = std::make_unique<MilkDecorator>(std::move(order));
order = std::make_unique<SugarDecorator>(std::move(order));

std::cout << order->getDescription() << ": $" << order->getCost();
// 출력: Coffee, Milk, Sugar: $2.7

사용 시기

다른 객체에 영향을 주지 않고 동적으로 객체에 책임을 추가해야 할 때, 또는 서브클래싱을 통한 기능 확장이 클래스의 폭발적인 증가를 초래할 때 데코레이터 패턴을 사용하세요.

직접 해보기

#include <iostream>
#include <string>
#include <memory>
#include "Pizza.h"
#include "ToppingDecorator.h"

using namespace std;

int main() {
    // 세 가지 입력 읽기 (yes 또는 no)
    string addCheese, addPepperoni, addMushrooms;
    cin >> addCheese;
    cin >> addPepperoni;
    cin >> addMushrooms;
    
    // TODO: std::unique_ptr를 사용하여 PlainPizza로 시작하기
    
    // TODO: addCheese가 "yes"인 경우, 피자를 CheeseTopping으로 감싸기
    
    // TODO: addPepperoni가 "yes"인 경우, 피자를 PepperoniTopping으로 감싸기
    
    // TODO: addMushrooms가 "yes"인 경우, 피자를 MushroomTopping으로 감싸기
    
    // TODO: 다음 형식으로 주문 출력하기:
    // Order: [description]
    // Total: $[cost]
    
    return 0;
}
quiz icon실력 점검

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

객체 지향 프로그래밍의 모든 레슨