Typescript로 다시 쓰는 GoF - Decorator

아홉번째태양·2023년 9월 11일
0

Decorator란?

우선, GoF에서 이야기하는 디자인 패턴으로서의 Decorator는 Java, Python, Typescript 등의 언어에 등장하는 Decorator@라는 기능과는 별개의 개념이다. 둘 다 상위 컨텐츠가 하위 컨텐츠를 감싸고 부가적인 기능을 제공하기에 같은 이름이 사용되지만, 다음의 두 가지 차이가 있다.

  • 먼저, 디자인 패턴으로서의 Decorator는 어디까지나 개념이고 패턴이며, 개발자가 직접 구현해야하는 코드 작성의 방식이다. 반면, 언어 기능으로서의 Decorator는 주로 다른 함수나 클래스를 감싸기 위해 @키워드와 함께 사용되는 언어 문법이다.
  • 그리고, 디자인 패턴으로서의 Decorator는 꾸며주는 대상이 되는 객체를 변형하지 않으며, 그 위에 새로운 기능을 추가하기만 한다. 하지만, 언어 기능으로서의 Decorator는 단순히 새로운 기능이나 역할을 추가하는 것뿐만이 아니라 기존의 코드가 동작하는 방식을 변형하기 위해 사용되기도 한다.

우선 먼저 예제코드부터 살펴보자.



Decorator 구현

Decorator에는 네 가지 객체가 등장한다.

  1. Component 컴포넌트
    기능을 추가할 때 핵심이 되는 역할로 주요 인터페이스들을 정의한다.
  2. ConcreteComponent 구체적인 컴포넌트
    Component의 인터페이스들을 구현한다.
  3. Decorator 장식자
    Component와 동일한 인터페이스를 가지지만, 장식 대상이 되는 Component도 가지고 있다.
  4. ConcreteDecorator 구체적인 장식자
    Decorator를 구현한다.

Decorator 패턴은 기존의 기능을 유지하면서 그 위에 새로운 기능을 덧대는 것이다. 그래서 아래처럼 처음에 주어진 문자열 주위에 장식을 이어붙여 나가는 예제를 만들기로 한다.

+-------------+
|Hello, world!|
+-------------+

Component

먼저 객체들의 중심이 되며 공통 인터페이스를 담은 객체 Display를 정의한다.

abstract class Display {
  abstract getColumns(): number
  abstract getRows(): number
  abstract getRowText(row: number): string
  
  show(): void {
    for (let i = 0; i < this.getRows(); i++) {
      console.log(this.getRowText(i))
    }
  }
}

타겟이되는 컨텐츠의 길이를 나타낼 수 있는 두 가지 메소드 getColumns, getRows와 컨텐츠의 각 줄을 출력하는 getRowText의 인터페이스만을 정의하고 구현은 하위 클래스에 맡긴다.

그리고 내용물을 출력해주는 메소드 show는 Template Method 패턴을 사용해 직접 구현한다.


ConcreteComponent

내용물이 되는 구체적인 컴포넌트를 구현한다. 여기서는 장식의 대상이 되는 문자열을 담는 객체 StringDisplay를 만든다.

class StringDisplay extends Display {
  constructor(
    private readonly string: string,
  ) {
    super()
    this.string = string
  }

  getColumns(): number {
    return this.string.length
  }

  getRows(): number {
    return 1
  }

  getRowText(row: number): string {
    return row === 0 ? this.string : '';
  }
}

Decorator

다음으로 내용물을 꾸며줄 수 있는 Decorator의 인터페이스 Border를 만든다. 이때, Border도 여전히 Component이기 때문에 앞에서 정의한 Display를 상속 받아야한다.

abstract class Border extends Display {
  constructor(
    protected readonly display: Display,
  ) {
    super()
    this.display = display
  }
}

동시에 장식하는 대상이 되는 또 다른 Component는 위임을 받아온다.


ConcreteDecorator

마지막으로 Decorator Border의 구현체를 만드는데, 내용물에 좌우 경계만 만들어주는 SideBorder와, 위아래좌우 전부 감싸주는 FullBorder 두 가지 객체를 만든다.

class SideBorder extends Border {
  constructor(
    display: Display,
    private readonly borderChar: string,
  ) {
    super(display)
    this.borderChar = borderChar
  }

  getColumns(): number {
    return 1 + this.display.getColumns() + 1
  }

  getRows(): number {
    return this.display.getRows()
  }

  getRowText(row: number): string {
    return this.borderChar + this.display.getRowText(row) + this.borderChar
  }
}

class FullBorder extends Border {
  constructor(
    display: Display,
  ) {
    super(display)
  }

  getColumns(): number {
    return 1 + this.display.getColumns() + 1
  }

  getRows(): number {
    return 1 + this.display.getRows() + 1
  }

  getRowText(row: number): string {
    if (row === 0) {
      return '+' + this.makeLine('-', this.display.getColumns()) + '+'
    } else if (row === this.display.getRows() + 1) {
      return '+' + this.makeLine('-', this.display.getColumns()) + '+'
    } else {
      return '|' + this.display.getRowText(row - 1) + '|'
    }
  }

  private makeLine(ch: string, count: number): string {
    return ch.repeat(count);
  }
}

실행

먼저 작성한 코드에서 각 구현체의 역할을 확인해보자.

const b1: Display = new StringDisplay('Hello, world!');
const b2: Display = new SideBorder(b1, '#');
const b3: Display = new FullBorder(b2);

b1.show();
b2.show();
b3.show();
Hello, world.
#Hello, world.#
+---------------+
|#Hello, world.#|
+---------------+

우선, b1, b2, b3 셋 다 Display라는 공통된 타입으로 정의되어 있다. 하지만, 각각을 출력하게되면 b1StringDisplay를 생성하면서 입력한 문자열을 출력하고, b2b1#을 양 옆에 붙여서 출력하며, 마지막으로 b3b2를 상자로 감싸서 출력한다.

이처럼 Decorator 객체들은 다른 Component에 덧대어서 부가적인 기능을 추가할 수 있으며, 아래 같은 내용의 작성도 가능하다.

const b4: Display = new SideBorder(
  new FullBorder(
    new FullBorder(
      new SideBorder(
        new FullBorder(
          new StringDisplay('Hello, world.')
        ),
        '*'
      )
    )
  ),
  '/'
);
b4.show();
/+-------------------+/
/|+-----------------+|/
/||*+-------------+*||/
/||*|Hello, world.|*||/
/||*+-------------+*||/
/|+-----------------+|/
/+-------------------+/


투과적 인터페이스

여기서 다시한번 주목할 점은, Decorator 패턴을 이용해서 상위 객체를 위임받아 사용을하지만, 본래 상위 객체는 변형되지 않은채 그 위에 새 기능만이 추가된다는 것이다. 즉, Decorator를 아무리 덧붙여도 원래의 내용물이 가려지지 않는다는 것인데, 이런 특징을 투과적 인터페이스라고 부르기도 한다.

앞서서 다루었던 Composite 패턴도 마찬가지로 투과적 인터페이스의 예시이며, Composite과 Decorator 둘 다 동일한 내용물이 다른 내용물을 감싸는 재귀적인 형태의 구조가 등장한다. 하자만, 두 패턴의 목적이 다르다는 것에 주의해야한다. Composite은 내용물과 그릇을 동일한 컨텐츠로 취급하는 반면, Decorator 패턴은 기존 기능에 바깥 테두리를 추가해 나가는 것만이 주 목적이다.




참고자료

Java언어로 배우는 디자인 패턴 입문 - 쉽게 배우는 Gof의 23가지 디자인패턴 (영진닷컴)
Javascript MDN
Typescript Documentation

0개의 댓글