[Structural Patterns] - Decorator

Lee Jeong Min·2022년 1월 10일
0

디자인 패턴

목록 보기
10/23
post-thumbnail

의도

Decorator 패턴은 동작을 포함하는 특수 래퍼 객체 안에 객체를 배치하여 객체에 동적으로 새 동작을 추가할 수 있는 Structural design 패턴이다.

대상 객체와 장식자 모두 동일한 인터페이스를 따르므로 decorator를 사용하여 객체를 계속해서 넘길 수 있다. 결과 객체는 모든 래퍼의 스택 동작을 가져온다.

스택 동작을 가져온다는 의미가 래퍼 객체안 객체가 있기 때문에 순서대로 메서드가 실행되면서 스택과 같은 구조처럼 순차적인 형태를 띈다는 의미인듯

문제

다른 프로그램이 사용자에게 중요한 이벤트를 알릴 수 있는 알림 라이브러리를 작업하고 있다고 가정하자.

라이브러리의 초기 버전은 몇 개의 필드, 생성자 및 단일 송신 메서드를 가진 Notifier 클래스에 기반을 두었다. 메서드는 클라이언트의 메시지 인수를 수락하고 생성자를 통해 통보자에게 전달된 전자 메일 목록으로 메시지를 보낼 수 있다. 클라이언트 역할을 하는 서드파티 앱은 알림 객체를 한 번 만들어 구성한 뒤 중요한 일이 발생할 때마다 사용하도록 되어 있다고 하자.

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

어느 순간 라이브러리 사용자가 이메일 알림 이상을 기대한다는 것을 알 수 있다. 그들 중 많은 사람들이 중요한 문제에 대한 SMS를 받고 싶어한다. 다른 사람들은 페이스북에서 알림을 받고 싶어하고, 기업 사용자들은 슬랙 알림을 받고 싶어할 것이다.

각 알림 유형은 알림의 하위 클래스로 구현된다.

Notifier 클래스를 확장하고 추가 알림 방법을 새 하위 클래스에 넣었다. 이제 클라이언트는 원하는 알림 클래스를 인스턴스화 하고 이후 모든 알림에 대해 사용하도록 되어있다.

그러나 누군가 "한 번에 여러 가지 알림 유형을 사용하는 것이 더 효율적이지 않을까? 라고 물어본다면 어떻게 대답할 것인가?

현재는 한 클래스 내에 여러 알림 방법을 결합한 특수 하위 클래스를 만들어 이 문제를 해결하려고 하였다. 그러나 이 접근 방식은 라이브러리 코드뿐만 아니라 클라이언트 코드도 엄청나게 부풀릴 것이 분명하다.

하위 클래스들의 조합이 엄청나게 많아 질 것이다.

이를 해결하기 위해 다른 방법을 찾아야 한다.

해결책

객체의 동작을 변경해야 할 때 가장 먼저 떠오르는 것은 클래스의 확장이다. 그러나 상속에는 몇 가지 주의사항이 있다.

  • 상속은 고정이다. 런타임에 기존 객체의 동작을 변경할 수 없다. 전체 객체를 다른 하위 클래스에서 생성된 다른 객체로만 바꿀 수 있다.
  • 하위 클래스는 부모 클래스를 하나만 가질 수 있다. 대부분의 언어에서 상속은 클래스가 여러 클래스의 동작을 동시에 상속하도록 허용하지 않는다.

이러한 주의사항을 극복하는 방법 중 하나는 상속 대신 집계(Aggregation) 또는 구성(Composition)을 사용하는 것이다. 두 가지 대안은 거의 같은 방식으로 동작한다. 한 객체는 다른 객체에 대한 참조를 가지고 일부 작업을 위임하는 반면, 상속을 이용하면 객체 자체는 슈퍼클래스의 동작을 이어받으며 해당 작업을 수행할 수 있다.

이 새로운 접근 방식을 사용하면 링크된 "헬퍼" 객체를 다른 객체로 쉽게 대체하여 런타임에 컨테이너의 동작을 변경할 수 있다. 객체는 여러 객체를 참조하고 모든 종류의 작업을 위임하는 다양한 클래스의 동작을 사용할 수 있다. 집합/구성은 Decorator를 포함한 많은 디자인 패턴의 핵심 원리이다.

상속 VS 집계

Wrapper는 패턴의 주요 아이디어를 명확하게 표현하는 Decorator 패턴의 대체 별명이다. Wrapper는 일부 대상 객체와 연결할 수 있는 객체이다. 래퍼에는 대상과 동일한 메서드의 집합이 포함되어 있고, 수신하는 모든 요청을 대상으로 위임한다. 그러나 래퍼들을 대상에 요청을 전달하기 전이나 후에 무언가를 함으로써 결과를 바꿀 수 있다.

언제 Wrapper가 Decorator가 될까? 말했듯이 래퍼는 래핑된 객체와 동일한 인터페이스를 구현한다. 그것이 고객의 관점에서 이 물건들이 동일한 이유이다. 래퍼의 참조 필드가 해당 인터페이스 다음에 오는 모든 객체를 허용하도록 한다. 이렇게 하면 객체를 여러 래퍼에 포함시켜 모든 래퍼의 결합된 동작을 객체에 추가할 수 있다.

알림 예제에서는 간단한 전자 메일 알림 동작을 기본 알림 클래스 안에 두고 다른 모든 알림 방법을 장식자로 변경해 보겠다.

다양한 알림 방법이 데코레이터가 된다.

클라이언트 코드는 기본 알림 객체를 클라이언트의 기본 설정과 일치하는 Decorator 집합으로 래핑해야 한다. 결과 객체가 스택으로 구조화 된다.

앱에서 복잡한 알림 Decorator 스택을 구성할 수 있다.

우리는 메시지 형식 지정이나 수신자 목록 구성과 같은 다른 행동에도 같은 접근법을 적용할 수 있다. 클라이언트는 다른 사용자와 동일한 인터페이스를 따르는 한 어떤 사용자 지정 Decorator로도 객체를 장식할 수 있다.

현실 유사성

여러 벌의 옷을 입으면 복합적인 효과를 볼 수 있다.

옷을 입는 것은 Decorator를 사용하는 예이다. 추울 때는 스웨터를, 그래도 춥다면 재킷을 입을 수 있다. 또한 비가오면 우비를 입는 것과 같다. 이 모든 옷들은 여러분의 기본적인 행동을 "확장"시켜주지만 여러분의 일부가 아니며 필요하지 않을 때 언제든지 쉽게 옷을 벗을 수 있다.

구조

  1. 구성 요소는 래퍼와 래퍼객체 모두에 대한 공통 인터페이스를 선언한다.

  2. 구체적인 구성요소는 래핑되는 객체의 클래스이다.

  3. 기본 Decorator 클래스에는 래핑된 객체를 참조하기 위한 필드가 있다. 필드의 유형은 구체적인 구성요소와 Decorator를 포함할 수 있도록 구성요소 인터페이스로 선언되어야 한다. 기본 Decorator는 모든 작업을 래핑된 객체에 위임한다.

  4. 구체적인 Decorator는 구성요소에 동적으로 추가할 수 있는 추가 동작을 정의한다. 구체적인 Decorator는 기본 Decorator의 메서드를 재지정하고 상위 메서드를 호출하기 전 또는 후에 동작을 실행한다.

  5. 클라이언트는 구성요소 인터페이스를 통해 모든 객체와 함께 작동하는 한 구성요소를 여러 층의 Decorator로 래핑할 수 있다.

적용가능성

  • 객체를 사용하는 코드를 중지하지 않고 런타임에 오브젝트에 추가 동작을 할당할 수 있는 경우 데코레이터 패턴을 사용해라.

  • 어색하거나 상속을 사용하여 객체의 동작을 확장할 수 없는 경우 패턴을 사용한다.

장단점

장점

  • 새 하위 클래스를 만들지 않고 객체의 동작을 확장할 수 있다.

  • 런타임에 객체에서 책임을 추가하거나 제거할 수 있다.

  • 객체를 여러 Decorator로 줄바꿈 하여 여러 동작을 결합할 수 있다.

  • 단일 책임 원칙. 여러 가지 가능한 동작 변형을 구현하는 단일 클래스를 여러 개의 작은 클래스로 나눌 수 있다.

단점

  • 래퍼 스택에서 특정 래퍼를 제거하는 것을 어렵다.

  • 데코레이터 스택의 순서에 따라 동작이 달라지지 않는 방식으로 데코레이터를 구현하기 어렵다.

  • 레이어의 초기 구성 코드는 보기 흉할 수 있다.

Decorator in TypeScript

TypeScript의 패턴 사용

복잡도: ★★☆

인기: ★★☆

사용 예: 데코레이터는 타입스크립트 코드, 특히 스트림과 관련된 코드에서 상당히 표준이다.(자주 사용된다는 말인듯)

식별: 데코레이터는 현재 클래스와 동일한 클래스 또는 인터페이스의 객체를 받아들이는 생성자 또는 메서드에서 인식될 수 있다.

index.ts

// 데코레이터에 의해 바뀔 수 있는 동작을 정의하는 기본 컴포넌트
interface Clothes {
  operation(): string;
}

// 수행 작업에 대한 기초 구현을 제공하는 구체적인 컴포넌트
class Cloth implements Clothes {
  public operation(): string {
    return '나는 가장 일반적인 옷이야';
  }
}

// 다른 컴포넌트와 같은 인터페이스를 따르는 기초 데코레이터
class Decorator implements Clothes {
  protected clothes: Clothes;

  constructor(clothes: Clothes) {
    this.clothes = clothes;
  }

  public operation(): string {
    return this.clothes.operation();
  }
}

class Sweater extends Decorator {
  public operation(): string {
    return `나는 스웨터 이고 따듯함 + 1을 해줘. 그리고 ${super.operation()}`;
  }
}

class Jacket extends Decorator {
  public operation(): string {
    return `나는 재킷이고 물 속성 + 1 을 해줘. 그리고 ${super.operation()}`;
  }
}

function clientCode(clothes: Clothes) {
  console.log(`RESULT: ${clothes.operation()}`);
}

const simple = new Cloth();
console.log("Client: I've got a simple Cloth:");
clientCode(simple);
console.log('');

const decorator1 = new Sweater(simple);
const decorator2 = new Jacket(decorator1);
console.log("Client: Now I've got a decorated component:");
clientCode(decorator2);

결과

요약

데코레이터 패턴은 특수 래퍼 객체안에 객체를 배치하여 동적으로 새 동작을 추가할 수 있게 한다.

이 패턴을 구현하기 위해 상속대신 Aggregation이나 Composition을 사용하고, 구체적인 컴포넌트의 클래스와 같은 인터페이스를 따르는 데코레이터를 생성하여 구체적인 데코레이터들을 만든다.

이러한 데코레이터들에 인수를 전달하여 사용이 가능하다.

참고 사이트

profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글