Menu
Coddy logo textTech

데코레이터 패턴

Coddy PHP 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 91개 중 77번째.

데코레이터 패턴(Decorator Pattern)은 객체들을 데코레이터라고 불리는 특별한 래퍼 객체 안에 감싸서 새로운 행동들을 동적으로 추가할 수 있게 해주는 구조적 디자인 패턴입니다. 컴파일 타임에 행동을 추가하는 상속과 달리, 데코레이터는 런타임에 행동을 추가합니다.

커피숍을 상상해 보세요. 기본 커피로 시작해서 우유, 설탕, 휘핑크림과 같은 추가 재료를 더합니다.

각각의 추가 요소는 원래의 커피를 감싸며 설명과 가격을 추가합니다. 핵심적인 통찰은 기본 커피와 데코레이터가 모두 동일한 인터페이스를 공유한다는 점입니다.

<?php
interface Coffee {
    public function getDescription(): string;
    public function getCost(): float;
}

class SimpleCoffee implements Coffee {
    public function getDescription(): string {
        return "Simple Coffee";
    }
    
    public function getCost(): float {
        return 2.00;
    }
}

데코레이터는 동일한 인터페이스를 구현하고 또 다른 Coffee 객체를 감싸줍니다:

<?php
class MilkDecorator implements Coffee {
    public function __construct(private Coffee $coffee) {}
    
    public function getDescription(): string {
        return $this->coffee->getDescription() . ", Milk";
    }
    
    public function getCost(): float {
        return $this->coffee->getCost() + 0.50;
    }
}

class SugarDecorator implements Coffee {
    public function __construct(private Coffee $coffee) {}
    
    public function getDescription(): string {
        return $this->coffee->getDescription() . ", Sugar";
    }
    
    public function getCost(): float {
        return $this->coffee->getCost() + 0.25;
    }
}

$coffee = new SimpleCoffee();
$coffee = new MilkDecorator($coffee);
$coffee = new SugarDecorator($coffee);

echo $coffee->getDescription() . " = $" . $coffee->getCost();

출력:

Simple Coffee, Milk, Sugar = $2.75

각 데코레이터는 이전 객체를 감싸며 체인을 형성합니다. 이 패턴을 사용하면 가능한 모든 조합에 대해 클래스를 생성하지 않고도 어떤 순서와 수량으로든 동작을 결합할 수 있습니다. 우유를 두 번 추가하고 싶으신가요? 그냥 두 번 감싸면 됩니다.

challenge icon

챌린지

쉬움

데코레이터 패턴(Decorator Pattern)을 사용하여 피자 주문 시스템을 구축해 보겠습니다. 기본 피자에서 시작하여 토핑을 동적으로 추가할 것입니다. 각 토핑은 피자를 감싸며(wrap), 레슨의 커피 예제와 마찬가지로 설명과 가격을 추가합니다.

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

  • Pizza.php — 모든 피자와 토핑에 대한 계약을 설정하는 Pizza 인터페이스를 정의합니다. 이 인터페이스는 getDescription(): stringgetPrice(): float라는 두 가지 메서드를 가져야 합니다.
  • BasePizza.php — Pizza 인터페이스를 포함하고 이를 구현하는 MargheritaPizza 클래스를 생성합니다. 이것은 설명이 "Margherita"이고 가격이 8.00인 기본 피자입니다.
  • Toppings.php — Pizza 인터페이스를 포함하고 이를 구현하는 세 개의 데코레이터 클래스를 생성합니다. 각 데코레이터는 Pizza 객체를 감싸며(생성자에서 이를 받음), 설명과 가격을 모두 향상시킵니다:
    • CheeseTopping — 설명에 ", Extra Cheese"를 추가하고 가격에 1.50을 추가합니다.
    • PepperoniTopping — 설명에 ", Pepperoni"를 추가하고 가격에 2.00을 추가합니다.
    • MushroomTopping — 설명에 ", Mushrooms"를 추가하고 가격에 1.25를 추가합니다.
  • main.php — BasePizza와 Toppings 파일을 포함합니다. 하나의 입력을 받게 됩니다: 추가할 토핑의 쉼표로 구분된 목록입니다 (예: "cheese,pepperoni" 또는 "mushroom,cheese,cheese").

    MargheritaPizza로 시작하세요. 입력된 각 토핑에 대해 적절한 데코레이터로 피자를 감싸세요:

    • "cheese"CheeseTopping으로 감쌉니다.
    • "pepperoni"PepperoniTopping으로 감쌉니다.
    • "mushroom"MushroomTopping으로 감쌉니다.

    모든 토핑을 적용한 후, 최종 피자의 설명과 가격을 다음 형식으로 출력하세요:

    [description] = $[price]

    가격은 소수점 둘째 자리까지 형식을 맞추세요.

동일한 토핑을 여러 번 적용할 수 있다는 점에 주목하세요. 더블 치즈를 주문하는 것은 단순히 CheeseTopping으로 두 번 감싸는 것을 의미합니다. 이러한 유연성이 바로 데코레이터 패턴의 강력함입니다!

치트 시트

데코레이터 패턴(Decorator Pattern)은 런타임에 객체를 데코레이터 객체로 감싸서 동작을 동적으로 추가하는 구조적 디자인 패턴입니다.

기본 객체와 데코레이터 모두 동일한 인터페이스를 구현합니다:

<?php
interface Coffee {
    public function getDescription(): string;
    public function getCost(): float;
}

class SimpleCoffee implements Coffee {
    public function getDescription(): string {
        return "Simple Coffee";
    }
    
    public function getCost(): float {
        return 2.00;
    }
}

데코레이터는 동일한 인터페이스 유형의 다른 객체를 감쌉니다:

<?php
class MilkDecorator implements Coffee {
    public function __construct(private Coffee $coffee) {}
    
    public function getDescription(): string {
        return $this->coffee->getDescription() . ", Milk";
    }
    
    public function getCost(): float {
        return $this->coffee->getCost() + 0.50;
    }
}

class SugarDecorator implements Coffee {
    public function __construct(private Coffee $coffee) {}
    
    public function getDescription(): string {
        return $this->coffee->getDescription() . ", Sugar";
    }
    
    public function getCost(): float {
        return $this->coffee->getCost() + 0.25;
    }
}

동작을 결합하기 위해 데코레이터를 체이닝합니다:

$coffee = new SimpleCoffee();
$coffee = new MilkDecorator($coffee);
$coffee = new SugarDecorator($coffee);

echo $coffee->getDescription() . " = $" . $coffee->getCost();
// Output: Simple Coffee, Milk, Sugar = $2.75

데코레이터는 모든 조합에 대해 별도의 클래스를 생성하지 않고도 여러 번, 그리고 어떤 순서로든 적용할 수 있습니다.

직접 해보기

<?php
require_once 'BasePizza.php';
require_once 'Toppings.php';

// 입력 읽기 - 쉼표로 구분된 토핑 목록
$input = trim(fgets(STDIN));
$toppings = explode(',', $input);

// 기본 MargheritaPizza로 시작
$pizza = new MargheritaPizza();

// TODO: 각 토핑을 반복하며 적절한 데코레이터로 피자를 감싸기
// - "cheese" -> CheeseTopping으로 감싸기
// - "pepperoni" -> PepperoniTopping으로 감싸기
// - "mushroom" -> MushroomTopping으로 감싸기

// TODO: 다음 형식으로 결과 출력: [description] = $[price]
// number_format()을 사용하여 가격을 소수점 둘째 자리까지 형식화

?>
quiz icon실력 점검

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

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