[Design Pattern] 데코레이터 패턴

olwooz·2023년 2월 18일
0

Design Pattern

목록 보기
10/22
객체들을 특별한 래퍼 객체로 감싸 새로운 행위를 부여해주는 구조 패턴

문제

유저들에게 중요 이벤트에 대해 알림을 보내주는 알림 라이브러리 가정

처음엔 Notifier 클래스에 이메일을 보내주는 send 메서드만 있었는데, 이메일 말고 SMS, Facebook, Slack 알림 등등도 지원해야 하는 상황

Notifier 클래스를 확장해 새로운 알림 메서드들을 서브클래스에 추가해줬는데, 사용자가 여러 알림 유형을 동시에 사용하고 싶어함 → 여러 개의 알림 방법을 하나의 클래스에 합쳐놓은 특별한 서브클래스를 만들려고 했는데, 결과적으로 코드가 엄청나게 불어나게 생김

해결책

특정 클래스의 행위를 바꾸고자 할 때 클래스를 확장하는 것이 가장 먼저 떠오르겠지만, 상속은 여러 주의해야 할 사항이 있음

  • 상속은 정적이기 때문에 이미 존재하는 객체의 행위를 런타임에 바꿀 수 없음, 객체 전체를 다른 서브클래스에서 생성된 객체로 대체하는 것만 가능
  • 서브클래스는 하나의 부모 클래스만 가질 수 있음, 대부분의 언어에서는 여러 클래스의 행위를 동시에 상속받을 수 없음

이 사항들을 극복하는 한 가지 방법은 상속 대신 Aggregation/Composition를 사용하는 것 → 두 대안 모두 한 객체가 다른 객체에 대한 참조를 가지고 있고 일부 작업을 위임함

이 방법을 통해, 연결된 헬퍼 객체를 다른 객체로 쉽게 대체하면서 런타임에 컨테이너의 행위를 변경할 수 있음

객체는 여러 객체에 대한 참조를 가지고 다양한 종류의 작업을 위임하면서 다양한 클래스의 행위를 사용할 수 있게 됨

Aggregation/Composition은 데코레이터를 포함한 많은 디자인 패턴들의 핵심 원칙

“Wrapper” - 데코레이터 패턴의 주요 아이디어를 표현하는 다른 별명

래퍼 - 대상 객체와 연결될 수 있는 객체로써 대상 객체와 같은 메서드들을 가지고 있고, 받는 모든 요청들을 대상 객체에 위임함

래퍼는 대상 객체에 요청을 보내기 전후로 무언가를 수행해 결과를 변경할 수 있음

래퍼를 데코레이터로 만들려면 래퍼의 참조 필드가 래핑된 객체의 인터페이스를 따르는 모든 객체를 받을 수 있게 해서 객체를 감싸는 모든 래퍼들의 행위들을 객체에 추가할 수 있게 하면 됨

클라이언트 코드는 기초 Notifier 객체를 클라이언트의 선호에 맞게 데코레이터들로 감싸야 함

결과 객체는 스택 구조를 띔

스택의 마지막 데코레이터는 클라이언트가 실제로 다루게 되는 객체

모든 데코레이터가 기본 Notifier와 같은 인터페이스를 구현하기 때문에, 나머지 클라이언트 코드는 다루는 객체가 순수 Notifier 객체인지 데코레이터로 장식된 객체인지 모름

메시지 포매팅이나 수신자 리스트 구성과 같은 다른 행위에도 같은 방식의 접근 가능

구조

1. 컴포넌트 - 래퍼와 래핑된 객체 모두에게 공통된 인터페이스 선언

2. Concrete 컴포넌트 - 래핑된 객체의 클래스, 데코레이터에 의해 바뀔 수 있는 기본 행위 정의

3. 기본 데코레이터 클래스 - 래핑된 객체를 참조하는 필드 보유, 
   필드의 타입은 컴포넌트 인터페이스로 선언해 concrete 컴포넌트와 데코레이터 모두를 가리킬 수 있게 함
   - 기본 데코레이터는 모든 행위를 래핑된 객체에 위임
    
4. Concrete 데코레이터 - 컴포넌트에 동적으로 추가될 수 있는 행위들을 정의
   - 기본 데코레이터의 메서드를 override해 부모 메서드 호출 전후로 특정 행위 실행
    
5. 클라이언트 - 모든 객체들을 컴포넌트 인터페이스를 통해 다루는 한, 
              컴포넌트들에 여러 개의 데코레이터를 씌울 수 있음

적용

객체를 사용하는 코드를 훼손하지 않으면서 객체에게 런타임에 추가 행위들을 부여해야 하는 경우

- 데코레이터는 비즈니스 로직을 계층 형태로 구성해 각 계층마다 데코레이터를 만들어 
  객체들을 다양한 조합의 데코레이터로 구성할 수 있게 해줌
- 객체들은 공통된 인터페이스를 따르기 때문에 클라이언트는 이 객체들을 모두 동등하게 취급할 수 있음

객체의 행위를 상속을 통해 확장하기 어렵거나 불가능한 경우

- 많은 언어들은 `final` 키워드로 클래스의 확장을 막을 수 있음 
  → `final` 클래스의 경우 데코레이터 패턴만이 유일하게 클래스의 기존 행위를 재사용할 수 있는 방법

구현방법

1. 비즈니스 도메인이 기본 컴포넌트를 여러 선택적 계층으로 감싸는 형태로 표현될 수 있는지 확인

2. 기본 컴포넌트와 선택적 계층 모두에게 공통된 메서드를 찾아 컴포넌트 인터페이스 안에 선언

3. concrete 컴포넌트 클래스를 생성하고 내부에 기본 행위를 정의

4. 기본 데코레이터 클래스 생성
    - 래핑된 객체에 대한 참조를 저장하는 필드 필요
    - 해당 필드는 컴포넌트 인터페이스 타입으로 선언해 concrete 컴포넌트와 데코레이터를 연결할 수 있게 함
    - 기본 데코레이터는 래핑된 객체에 모든 작업을 위임해야 함
    
5. 모든 클래스가 컴포넌트 인터페이스를 구현하게 함

6. 기본 데코레이터를 확장한 concrete 데코레이터 생성
    - 부모 메서드를 호출하기 전후로 특정 행위를 수행해야 함
    
7. 클라이언트 코드는 데코레이터들을 생성해 클라이언트가 필요로 하는 방식으로 조합해야 함

장단점

장점

- 새로운 서브클래스를 만들지 않고 객체의 행위 확장 가능
- 런타임에 객체의 책임 추가/제거 가능
- 객체를 여러 개의 데코레이터로 감싸 여러 행위를 합칠 수 있음
- SRP - 행위의 여러 변형들을 구현하는 모놀리식 클래스를 여러 개의 작은 클래스들로 나눌 수 있음

단점

- 래퍼 스택에서 특정 래퍼를 제거하기 어려움
- 데코레이터의 행위가 데코레이터 스택의 순서에 의존하지 않도록 구현하기가 어려움
- 초기 레이어 설정 코드가 보기 안 좋을 수 있음

다른 패턴과의 관계

- 어댑터 - 기존 객체의 인터페이스 변경, 
  데코레이터 - 인터페이스를 변경하지 않고 객체 향상, 재귀 합성 지원
  
- 어댑터 - 래핑된 객체에 다른 인터페이스 제공, 
  프록시 - 같은 인터페이스 제공, 
  데코레이터 - 향상된 인터페이스 제공
  
- 책임 연쇄 패턴과 데코레이터 패턴은 비슷한 클래스 구조를 가지지만 차이점들 존재
  - 둘 다 재귀적 합성에 의존해 일련의 객체에 실행 전달
  - 책임 연쇄 핸들러 - 독립적으로 임의의 작업을 실행할 수 있고, 아무 때나 요청 전달을 멈출 수 있음
  - 데코레이터 - 기본 인터페이스를 유지하며 객체의 행위를 확장할 수 있고, 요청의 흐름을 끊을 수 없음
   
- 컴포지트 패턴과 데코레이터 패턴 모두 재귀적 구성에 의존해 다수의 객체들을 정리하기 때문에
  비슷한 구조 다이어그램을 가짐
  - 데코레이터 패턴 - 컴포지트 패턴과 비슷하지만 하나의 자식 컴포넌트만 가짐
  - 데코레이터 패턴 - 래핑된 객체에 추가적인 책임을 부여
  - 컴포지트 패턴 - 자식들의 결과를 종합하기만 함
  - 서로 협력 가능 - 데코레이터를 사용해 컴포지트 트리의 특정 객체의 행위를 확장
   
- 컴포지트 패턴과 데코레이터 패턴을 많이 사용하는 설계는 프로토타입 패턴을 통해 이득을 볼 수 있음 
  → 복잡한 구조를 처음부터 재구축하는 대신 복제할 수 있게 해줌
  
- 데코레이터 패턴 - 객체의 피부를 변경
  전략 패턴 - 객체의 내장 변경

- 데코레이터와 프록시는 비슷한 구조를 가지지만 매우 다른 의도를 가짐
  - 두 패턴 모두 한 객체가 일부 작업을 다른 객체에 위임하는 합성 원칙 기반
  - 프록시 - 서비스 객체의 생명 주기를 주로 직접 관리
  - 데코레이터 - 합성이 클라이언트에 의해 제어됨

TypeScript 예제

/**
 * The base Component interface defines operations that can be altered by
 * decorators.
 */
interface Component {
    operation(): string;
}

/**
 * Concrete Components provide default implementations of the operations. There
 * might be several variations of these classes.
 */
class ConcreteComponent implements Component {
    public operation(): string {
        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 implements Component {
    protected component: Component;

    constructor(component: Component) {
        this.component = component;
    }

    /**
     * The Decorator delegates all work to the wrapped component.
     */
    public operation(): string {
        return this.component.operation();
    }
}

/**
 * Concrete Decorators call the wrapped object and alter its result in some way.
 */
class ConcreteDecoratorA extends Decorator {
    /**
     * Decorators may call parent implementation of the operation, instead of
     * calling the wrapped object directly. This approach simplifies extension
     * of decorator classes.
     */
    public operation(): string {
        return `ConcreteDecoratorA(${super.operation()})`;
    }
}

/**
 * Decorators can execute their behavior either before or after the call to a
 * wrapped object.
 */
class ConcreteDecoratorB extends Decorator {
    public operation(): string {
        return `ConcreteDecoratorB(${super.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.
 */
function clientCode(component: Component) {
    // ...

    console.log(`RESULT: ${component.operation()}`);

    // ...
}

/**
 * This way the client code can support both simple components...
 */
const simple = new ConcreteComponent();
console.log('Client: I\'ve got a simple component:');
clientCode(simple);
console.log('');

/**
 * ...as well as decorated ones.
 *
 * Note how decorators can wrap not only simple components but the other
 * decorators as well.
 */
const decorator1 = new ConcreteDecoratorA(simple);
const decorator2 = new ConcreteDecoratorB(decorator1);
console.log('Client: Now I\'ve got a decorated component:');
clientCode(decorator2);
// Output.txt

Client: I've got a simple component:
RESULT: ConcreteComponent

Client: Now I've got a decorated component:
RESULT: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))

참고 자료: Refactoring.guru

0개의 댓글