디자인 패턴

Decorator 패턴

Keisa 2024. 7. 11. 10:26

의도

데코레이터는 객체들을 새로운 행동들을 포함한 특수 래퍼 객체들 내에 넣어서 위 행동들을 해당 객체들에 연결시키는 구조적 디자인 패턴입니다.

문제

당신이 알림 라이브러리를 만들고 있다고 상상해 보세요. 이 알림 라이브러리의 목적은 다른 프로그램들이 사용자들에게 중요한 이벤트들에 대해 알릴 수 있도록 하는 것입니다.

이 라이브러리의 초기 버전은 Notifier​(알림자) 클래스를 기반으로 했으며, 이 클래스에는 몇 개의 필드들, 하나의 생성자 그리고 단일 send​(전송) 메서드만 있었습니다. 이 메서드는 클라이언트로부터 메시지 인수를 받은 후 그 메세지를 알림자의 생성자를 통해 알림자에게 전달된 이메일 목록으로 보낼 수 있습니다. 또 클라이언트 역할을 한 타사 앱은 알림자 객체를 한번 생성하고 설정한 후 중요한 일이 발생할 때마다 사용하게 되어 있었습니다.

프로그램은 알림자 클래스를 사용하여 미리 정의된 이메일들의 집합에 중요한 이벤트에 대한 알림을 보낼 수 있습니다.

당신은 어느 시점에서 라이브러리 사용자들이 이메일 알림 이상을 기대한다는 것을 알게 됩니다. 그들 중 많은 사용자는 중요한 사안에 대한 SMS 문자 메시지를 받고 싶어 했고, 다른 사용자들은 페이스북 알림을 받고 싶어 했으며 기업 사용자들은 슬랙 알림을 받고 싶어 했습니다.

각 알림 유형은 알림자의 자식 클래스로 구현됩니다.

이는 표면상 별로 어렵지 않아 보입니다. 당신은 Notifier​(알림자) 클래스를 확장하고 추가 알림 메서드들을 새 자식 클래스들에 넣어 이제 클라이언트가 사용자가 원하는 알림 클래스를 인스턴스화하고 모든 추가 알림에 사용하도록 앱을 설계했습니다.

그런데 누군가 당신에게 물었습니다. '여러 유형의 알림을 한 번에 사용할 수는 없나요? 집에 불이라도 난다면 사용자들은 모든 채널에서 정보를 받고 싶어 할 겁니다.'

이 문제를 해결하기 위해 당신은 하나의 클래스 내에서 여러 알림 메서드를 합성한 특수 자식 클래스들을 만들었으나, 이 접근 방식은 라이브러리 코드뿐만 아니라 클라이언트 코드도 엄청나게 부풀릴 것이라는 사실이 금세 명백해졌습니다.

자식 클래스들의 합성으로 인한 클래스 수의 폭발

이제 당신은 알림 클래스들의 수가 지나치게 많아지지 않도록 알림 클래스들을 구성하는 다른 방법을 찾아야 합니다.

 해결책

객체의 동작을 변경해야 할 때 가장 먼저 고려되는 방법은 클래스의 확장입니다. 그러나 상속에는 당신이 심각하게 주의해야 할 몇 가지 사항들이 있습니다.

  • 상속은 정적입니다: 당신은 런타임​(실행시간) 때 기존 객체의 행동을 변경할 수 없습니다. 당신은 전체 객체를 다른 자식 클래스에서 생성된 다른 객체로만 바꿀 수 있습니다.
  • 자식 클래스는 하나의 부모 클래스만 가질 수 있습니다. 대부분 언어에서의 상속은 클래스가 동시에 여러 클래스의 행동을 상속하도록 허용하지 않습니다.

이러한 주의 사항을 극복하는 방법의 하나는  대신   또는 을 사용하는 것입니다. 두 대안 모두 거의 같은 방식으로 작동합니다: 집합 관계에서는 한 객체가 다른 객체에 대한   일부 작업을 위임하는 반면, 상속을 사용하면 객체 자체가 부모 클래스에서 행동을 상속한 후 해당 작업을   .

이 새로운 접근 방식을 사용하면 연결된 '도우미' 객체를 다른 객체로 쉽게 대체하여 런타임 때 컨테이너의 행동을 변경할 수 있습니다. 객체는 여러 클래스의 행동들을 사용할 수 있고, 여러 객체에 대한 참조들이 있으며 이 객체들에 모든 종류의 작업을 위임합니다. 집합 관계/합성은 데코레이터를 포함한 많은 디자인 패턴의 핵심 원칙입니다. 그러면 이제 다시 이 패턴에 대하여 살펴봅시다.

상속과 집합 관계의 차이점.

'래퍼'는 패턴의 주요 아이디어를 명확하게 표현하는 데코레이터 패턴의 별명입니다. 는 일부  객체와 연결할 수 있는 객체입니다. 래퍼에는 대상 객체와 같은 메서드들의 집합이 포함되어 있으며, 래퍼는 자신이 받는 모든 요청을 대상 객체에 위임합니다. 그러나 래퍼는 이 요청을 대상에 전달하기 전이나 후에 무언가를 수행하여 결과를 변경할 수 있습니다.

그러면 간단한 래퍼는 언제 진정한 데코레이터가 될 수 있을까요? 앞서 언급했듯이 래퍼는 래핑된 객체와 같은 인터페이스를 구현합니다. 그러므로 클라이언트의 관점에서 이러한 객체들은 같습니다. 이제 래퍼의 참조 필드가 해당 인터페이스를 따르는 모든 객체를 받도록 하세요. 이렇게 하면 여러 래퍼로 객체를 포장해서 모든 래퍼들의 합성된 행동들을 객체에 추가할 수 있습니다.

이제 당신의 알림 라이브러리에서 기초 Notifier 클래스 내에 있는 간단한 이메일 알림 행동은 그대로 두고 다른 모든 알림 메서드들을 데코레이터로 바꾸어 봅시다.

다양한 알림 메서드들이 데코레이터가 됩니다.

클라이언트 코드는 기초 알림자 객체를 클라이언트의 요구사항들과 일치하는 데코레이터들의 집합으로 래핑해야 합니다. 위 결과 객체들은 스택으로 구성됩니다.

앱들은 알림 데코레이터들의 복잡한 스택들을 설정할 수 있습니다.

스택의 마지막 데코레이터는 실제로 클라이언트와 작업하는 객체입니다. 모든 데코레이터들은 기초 알림자와 같은 인터페이스를 구현하므로 나머지 클라이언트 코드는 자신이 '순수한' 알림자 객체와 작동하든 데코레이터로 장식된 알림자 객체와 함께 작동하든 상관하지 않습니다.

메시지 형식 지정 또는 수신자 리스트 작성과 같은 다른 행동들에도 같은 접근 방식을 적용할 수 있습니다. 클라이언트는 객체가 다른 객체들과 같은 인터페이스를 따르는 한 객체를 모든 사용자 지정 데코레이터로 장식할 수 있습니다.

 실제상황 적용

여러 벌의 옷을 입으면 복합 효과를 얻을 수 있습니다.

옷을 입는 것은 데코레이터 패턴을 사용하는 예입니다. 당신은 추울 때 스웨터로 몸을 감쌉니다. 스웨터를 입어도 춥다면 위에 재킷을 입고, 또 비가 오면 비옷을 입습니다. 이 모든 옷은 기초 행동을 '확장'하지만, 당신의 일부가 아니기에 필요하지 않을 때마다 옷을 쉽게 벗을 수 있습니다.

 구조

  1. 컴포넌트는 래퍼들과 래핑된 객체들 모두에 대한 공통 인터페이스를 선언합니다.
  2. 구상 컴포넌트는 래핑되는 객체들의 클래스이며, 그는 기본 행동들을 정의하고 해당 기본 행동들은 데코레이터들이 변경할 수 있습니다.
  3. 기초 데코레이터 클래스에는 래핑된 객체를 참조하기 위한 필드가 있습니다. 필드의 유형은 구상 컴포넌트들과 구상 데코레이터들을 모두 포함할 수 있도록 컴포넌트 인터페이스로 선언되어야 합니다. 그 후 기초 데코레이터는 모든 작업들을 래핑된 객체에 위임합니다.
  4. 구상 데코레이터들은 컴포넌트들에 동적으로 추가될 수 있는 추가 행동들을 정의합니다. 그들은 기초 데코레이터의 메서드를 오버라이드​(재정의)​하고 해당 행동을 부모 메서드를 호출하기 전이나 후에 실행합니다.
  5. 클라이언트는 아래에 언급한 데코레이터들이 컴포넌트 인터페이스를 통해 모든 객체와 작동하는 한 컴포넌트들을 여러 계층의 데코레이터들로 래핑할 수 있습니다.

 의사코드

이 예시에서 데코레이터 패턴은 민감한 데이터를 해당 데이터를 실제로 사용하는 코드와 별도로 압축하고 암호화할 수 있도록 합니다.

암호화 및 압축화 데코레이터들의 예.

이 애플리케이션은 데이터 소스 객체를 한 쌍의 데코레이터로 래핑합니다. 두 래퍼 모두 디스크에 데이터를 쓰고 읽는 방식들을 변경합니다.

  • 데이터가 디스크에 기록되기 직전에 데코레이터들은 데이터를 암호화하고 압축합니다. 원래 클래스는 위 변경 사항에 대해 알지 못한 채 암호화되고 보호된 데이터를 파일에 씁니다.
  • 데이터는 디스크에서 읽힌 직후 같은 데코레이터들을 거쳐 가며, 이 데코레이터들은 데이터의 압축을 풀고 디코딩합니다.

데코레이터와 데이터 소스 클래스는 같은 인터페이스를 구현하므로 클라이언트 코드에서 모두 상호 교환이 가능합니다.

/**
 * The base Component interface defines operations that can be altered by
 * decorators.
 */
class Component 
{
 public:
  virtual ~Component() {}
  virtual std::string Operation() const = 0;
};
/**
 * Concrete Components provide default implementations of the operations. There
 * might be several variations of these classes.
 */
class ConcreteComponent : public Component 
{
 public:
  std::string Operation() const override 
  {
    return "ConcreteComponent";
  }
};
/**
 * The base Decorator class follows the same interface as the other components.
 * The primary purpose of this class is to define the wrapping interface for all
 * concrete decorators. The default implementation of the wrapping code might
 * include a field for storing a wrapped component and the means to initialize
 * it.
 */
class Decorator : public Component
{
  /**
   * @var Component
   */
 protected:
  Component* component_;

 public:
  Decorator(Component* component) : component_(component)
  {}
  /**
   * The Decorator delegates all work to the wrapped component.
   */
  std::string Operation() const override 
  {
    return this->component_->Operation();
  }
};
/**
 * Concrete Decorators call the wrapped object and alter its result in some way.
 */
class ConcreteDecoratorA : public Decorator 
{
  /**
   * Decorators may call parent implementation of the operation, instead of
   * calling the wrapped object directly. This approach simplifies extension of
   * decorator classes.
   */
 public:
  ConcreteDecoratorA(Component* component) : Decorator(component) 
  {}
  
  std::string Operation() const override 
  {
    return "ConcreteDecoratorA(" + Decorator::Operation() + ")";
  }
};
/**
 * Decorators can execute their behavior either before or after the call to a
 * wrapped object.
 */
class ConcreteDecoratorB : public Decorator 
{
 public:
  ConcreteDecoratorB(Component* component) : Decorator(component) 
  {}

  std::string Operation() const override 
  {
    return "ConcreteDecoratorB(" + Decorator::Operation() + ")";
  }
};
/**
 * The client code works with all objects using the Component interface. This
 * way it can stay independent of the concrete classes of components it works
 * with.
 */
void ClientCode(Component* component) 
{
  // ...
  std::cout << "RESULT: " << component->Operation();
  // ...
}

int main() 
{
  /**
   * This way the client code can support both simple components...
   */
  Component* simple = new ConcreteComponent;
  std::cout << "Client: I've got a simple component:\n";
  ClientCode(simple);
  std::cout << "\n\n";
  /**
   * ...as well as decorated ones.
   *
   * Note how decorators can wrap not only simple components but the other
   * decorators as well.
   */
  Component* decorator1 = new ConcreteDecoratorA(simple);
  Component* decorator2 = new ConcreteDecoratorB(decorator1);
  std::cout << "Client: Now I've got a decorated component:\n";
  ClientCode(decorator2);
  std::cout << "\n";

  delete simple;
  delete decorator1;
  delete decorator2;

  return 0;
}

 

'디자인 패턴' 카테고리의 다른 글

Flyweight 패턴  (0) 2024.07.25
Facade 패턴  (0) 2024.07.25
Composite 패턴  (0) 2024.07.11
Bridge 패턴  (0) 2024.07.03
Adapter 패턴  (0) 2024.06.29