Typescript로 다시 쓰는 GoF - Template Method

아홉번째태양·2023년 8월 4일
0

Template Method란?

어떤 모양이나 형식과 같은 틀만 정해져있고 어떻게가 비어있는 것을 템플릿이라고 한다. 그리고 Template Method는 이름 그대로 템플릿이 되는 메소드를 추상 메소드Abstract Method를 사용해 뼈대만 만들고 하위 클래스에서 구현방식을 직접 정의하는 패턴이다.



Template Method 구현

Template Method를 구성하는 객체는 비교적 단순하게 두 가지 객체가 필요하다.

  1. Abstract Class(추상 클래스)
    Template Method를 구현하는 객체이며, 해당 템플릿 메소드에서 사용하는 추상 메소드를 선언한다.

  2. Concrete Class(구현 클래스)
    Abstract Class에서 정의한 추상 메소드를 구현하여, Template Method의 실제 동작을 결정한다.


주어진 문자 혹은 문자열을 5번 반복해서 출력하는 프로그램을 만들어보자.


Abstract Class

open, print, close 3가지 추상 메소드를 가지고 이 메소드들의 호출 로직만을 정의하는 display를 구현하는 추상 클래스 AbstractDisplay를 만든다.

abstract class AbstractDisplay {
  abstract open(): void;
  abstract print(): void;
  abstract close(): void;

  display(): void {
    this.open();
    for (let i = 0; i < 5; i += 1) {
      this.print();
    }
    this.close();
  }
}

display라는 코드가 구현되어있지만, 이 내용만 봐서는 이 메소드가 무엇을 출력하기 위함인지, 혹은 정말 이름 그대로 출력하는 메소드가 맞는지 아무것도 알 수가 없다.

따라서, 실제 동작을 구현하는 하위 클래스가 필요하다.


Concrete Class - CharDisplay

먼저, 단순히 하나의 문자를 받아서 처리하는 클래스를 만들어보자.

class CharDisplay extends AbstractDisplay {
  private line: string[] = [];
  constructor(
    private char: string
  ) {
    super();
    this.char = char;
  }

  open() {
    this.line.push('<<');
  }

  print() {
    this.line.push(this.char);
  }

  close() {
    this.line.push('>>');
    console.log(this.line.join(''));
  }
}

CharDisplay는 처음 인스턴스화를 할 때 받은 문자 char를 바탕으로 이를 출력하거나 혹은 다른 문자를 출력하는 코드를 AbstractDisplay의 세 메소드 open, print, close에 구현을 했다.

참고로, 자바스크립트에서 console.log는 새로 호출할때마다 다음 줄에 출력을 하기 때문에 한줄에 모아서 출력을 하려면 문자열을 합치는 작업이 따로 필요하다.

이로서 CharDisplay를 통해 display를 호출하면 완성된 기능이 나온다.

const displayChar = new CharDisplay('H');
displayChar.display();
<<HHHHH>>

Concrete Class - StringDisplay

이번에는 같은 Template Metho를 다르게 구현해보자.

class StringDisplay extends AbstractDisplay {
  constructor(
    private string: string,
    private width: number
  ) {
    super();
    this.string = string;
    this.width = width;
  }

  open() {
    this.printLine();
  }

  print() {
    console.log(`|${this.string}|`);
  }

  close() {
    this.printLine();
  }

  private printLine() {
    const line = ['+'];
    for (let i = 0; i < this.width; i += 1) {
      line.push('-');
    }
    line.push('+');
    console.log(line.join(''));
  }
}

문자 하나 대신 문자열을 넣고 문자열을 상자 형태로 출력하도록 구현했다.

const displayString = new StringDisplay('Hello, World', 10);
displayString.display();
+----------+
|Hello, World|
|Hello, World|
|Hello, World|
|Hello, World|
|Hello, World|
+----------+

이처럼 템플릿 메소드는 구현체에 따라 실행 결과가 달라질 수 있다.


Concrete Class - PrintSum

하지만 이렇게 구현 로직이 자유롭다는 것은 경우에 따라서 본래 의도와는 다른 기능이 Template Method를 통해 구현될 수도 있음을 의미한다.

예를들어, print라는 메소드가 반드시 무엇인가를 출력하지 않을 수도 있다.

class PrintSum extends AbstractDisplay {
  private sum: number = 0;

  constructor(
    private n: number,
  ) {
    super();
    this.n = n;
  }

  open() {
    this.sum = 0;
  }

  print() {
    this.sum += this.n;
  }

  close() {
    console.log(this.sum);
  }
}

const printSum = new PrintSum(3);
printSum.display();
15

이처럼 Template Method의 동작은 같지만 완전히 구현하는 기능이 달라져버렸다.



Template Method 언제 쓸까?

Template Method 패턴처럼 구체적인 구현의 책임을 하위 클래스에 위임하는 것을 하위 클래스의 책임 SubClass Responsibility라고 한다.

이때, 상위 클래스는 추상 메소드를 선언함으로서,

  • 하위 클래스에서 그 메소드를 구현하기를 기대한다.
  • 하위 클래스는 약간의 메소드를 추가하는 것만으로 새로운 기능을 추가할 수 있다.

이를 통해 얻을 수 있는 이득은 다음과 같다.

코드 중복

Template Method의 장점은 유사한 기능을 구현할 때 뼈대만 구현하고 세부 로직은 하위 클래스에 위임함으로서 코드의 반복을 피할 수 있다.


간단한 디버깅

설령 기능에 문제가 생기더라도 수정은 오직 구현체 하나 혹은 뼈대 로직만 수정을 하면된다. 만일, 유사한 종류의 구현체 ConcreteClass1, ConcreteClass2, ConcreteClass3을 만들고 어느 하나가 동작에 문제가 생겼고 뼈대 로직에 문제가 있던거라면 모든 구현 클래스를 다 수정해야만 한다.


LSP 원칙

또한, Template Method를 통해서 구현한 하위 클래스들은 instance of 같은 키워드를 사용할 때 하위 클래스가 아닌 상위 클래스를 특정해서 사용할 수 있는 경우에 더 빛을 발한다. 이렇게 상위 클래스를 지칭함으로서 하위 클래스 어느 것을 대입해도 동작할 수 있게 만드는 것을 LSP 원칙(The Liskov Substitution Principle)이라고 한다.



상위클래스와 하위클래스의 밸런스

하지만 상위 클래스의 구현을 하위 클래스에 위임하는 것이 언제나 좋은 것은 아니다.

하위 클래스에 많은 책임을 넘기게되면 되려 하위 클래스의 작성이 힘들어지고 하위클래스들에서 처리 기술이 중복될 수도 있다. 반대로 상위 클래스에서 너무 많은 기술을 하게되면 하위 클래스의 작성은 편해지지만 자유도는 떨어진다.

따라서, 상황에 따라 적당하게 이 밸런스를 조절하는 것이 중요하며, 이는 상위 클래스, 혹은 하위 클래스의 코드가 변경됨에 따라 언제든 유동적으로 리팩토링이 가능해야 한다.




참고자료

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

0개의 댓글