데코레이터 decorator 패턴 - 자바스크립트 예제

SW설계
읽는데 5분 소요
처음 쓰여진 날: 2025-09-01
마지막 수정일: 2025-09-01

요약

데코레이터 decorator 패턴을 자바스크립트 코드와 함께 알아봅니다. 정보처리기사 대비 문제가 포함되어있습니다.

데코레이터 패턴 요약

패턴 종류핵심 키워드
데코레이터 (Decorator)동적으로 기능 추가, 객체 조합, 상속의 대안
데코레이터 패턴 감자
데코레이터 패턴 감자

데코레이터 (Decorator) 패턴

데코레이터 패턴은 객체의 기존 코드를 수정하지 않고도 동적으로 새로운 기능이나 책임을 추가할 수 있게 해주는 구조 패턴입니다. 이름처럼 객체를 장식(decorate)하여 기능을 확장하는 방식입니다. 상속을 통해 기능을 확장할 수도 있지만, 상속은 정적인 방식이라 모든 조합에 대한 서브클래스를 만들어야 하는 '클래스 폭발(class explosion)' 문제를 야기할 수 있습니다. 데코레이터 패턴은 이러한 문제를 해결하는 유연한 대안입니다.

현실 세계의 예로 '커피 주문'을 생각해볼 수 있습니다. 기본 커피(아메리카노)에 우유를 추가하고(카페라떼), 거기에 시럽을 추가하고(바닐라 라떼), 휘핑 크림을 올리는(아인슈페너)처럼, 기본 객체에 여러 '장식(옵션)'을 동적으로 추가해 새로운 객체를 만드는 것과 같습니다.

기본 구조

데코레이터 패턴은 다음 요소들로 구성됩니다.

  • Component: 장식될 객체와 장식하는 객체(데코레이터)가 모두 구현해야 할 공통 인터페이스(또는 추상 클래스)입니다.
  • ConcreteComponent: 장식의 대상이 되는 핵심 기능을 가진 구체적인 클래스입니다. (예: 기본 커피)
  • Decorator: Component 인터페이스를 구현하면서, 내부에 Component 객체에 대한 참조를 가집니다. 이 참조를 통해 장식할 객체와 연결됩니다.
  • ConcreteDecorator: Decorator를 상속받아 실제로 새로운 기능이나 책임을 추가하는 클래스입니다. (예: 우유 추가, 시럽 추가)

예시: 커피 주문 시스템

다양한 옵션을 추가할 수 있는 커피 주문 시스템을 만들어 보겠습니다.

먼저 모든 커피 객체가 따를 Coffee 인터페이스(여기서는 클래스로 정의)를 만듭니다.

javascript
// Component: 공통 인터페이스
class Coffee {
  cost() {
    throw new Error("cost()는 반드시 구현되어야 합니다.");
  }

  getDescription() {
    throw new Error("getDescription()은 반드시 구현되어야 합니다.");
  }
}

가장 기본이 되는 커피, SimpleCoffee를 만듭니다.

javascript
// ConcreteComponent: 장식될 기본 객체
class SimpleCoffee extends Coffee {
  cost() {
    return 5; // 기본 커피 가격 5
  }

  getDescription() {
    return "기본 커피";
  }
}

이제 데코레이터의 기반이 될 추상 클래스를 만듭니다. 이 클래스는 다른 Coffee 객체를 감쌀(wrap) 것입니다.

javascript
// Decorator: 다른 Coffee 객체를 감싸는 역할
class CoffeeDecorator extends Coffee {
  constructor(coffee) {
    super();
    this._coffee = coffee; // 감쌀 Coffee 객체를 저장
  }

  cost() {
    return this._coffee.cost(); // 감싼 객체의 cost()를 호출
  }

  getDescription() {
    return this._coffee.getDescription(); // 감싼 객체의 getDescription()을 호출
  }
}

이제 구체적인 데코레이터들을 만듭니다. WithMilkWithSugar는 각각 우유와 설탕을 추가하는 역할을 합니다.

javascript
// ConcreteDecorator A: 우유 추가
class WithMilk extends CoffeeDecorator {
  constructor(coffee) {
    super(coffee);
  }

  cost() {
    return super.cost() + 2; // 기본 가격에 우유 가격 추가
  }

  getDescription() {
    return super.getDescription() + ", 우유 추가";
  }
}

// ConcreteDecorator B: 설탕 추가
class WithSugar extends CoffeeDecorator {
  constructor(coffee) {
    super(coffee);
  }

  cost() {
    return super.cost() + 1; // 기본 가격에 설탕 가격 추가
  }

  getDescription() {
    return super.getDescription() + ", 설탕 추가";
  }
}

// 클라이언트 코드
let coffee = new SimpleCoffee();
console.log(`${coffee.getDescription()} - 가격: ${coffee.cost()}`);
// 출력: 기본 커피 - 가격: 5

// 우유를 추가해봅시다.
coffee = new WithMilk(coffee);
console.log(`${coffee.getDescription()} - 가격: ${coffee.cost()}`);
// 출력: 기본 커피, 우유 추가 - 가격: 7

// 거기에 설탕도 추가해봅시다.
coffee = new WithSugar(coffee);
console.log(`${coffee.getDescription()} - 가격: ${coffee.cost()}`);
// 출력: 기본 커피, 우유 추가, 설탕 추가 - 가격: 8

// 순서를 바꿔서 설탕을 먼저 추가할 수도 있습니다.
let anotherCoffee = new SimpleCoffee();
anotherCoffee = new WithSugar(anotherCoffee);
anotherCoffee = new WithMilk(anotherCoffee);
console.log(
  `${anotherCoffee.getDescription()} - 가격: ${anotherCoffee.cost()}`
);
// 출력: 기본 커피, 설탕 추가, 우유 추가 - 가격: 8

이처럼 데코레이터 패턴을 사용하면, SimpleCoffee 클래스를 전혀 수정하지 않고도 WithMilk, WithSugar 같은 새로운 기능을 동적으로 계속 추가할 수 있습니다. 만약 '휘핑 크림 추가', '샷 추가' 등의 기능이 필요하다면 새로운 데코레이터 클래스만 만들어주면 됩니다.

데코레이터 패턴 중요 키워드

  • 객체를 감싸서(wrap) 새로운 책임을 추가합니다.
  • 객체의 결합(SimpleCoffee, WithMilk, WithSugar)을 통해 기능을 확장합니다.
  • 상속의 유연한 대안입니다.
  • 동적으로 기능을 추가하거나 제거할 수 있습니다.
  • 핵심 기능과 추가 기능이 동일한 인터페이스를 따릅니다.
  • 데코레이터의 순서가 결과에 영향을 줄 수 있습니다.
문제
객체의 기존 코드를 수정하지 않고도 동적으로 새로운 기능이나 책임을 추가할 수 있게 해주는 패턴으로, 객체를 감싸서(wrap) 기능을 확장하며 상속의 유연한 대안을 제공하는 디자인 패턴은?
보기
답변
정답정답 보기