[타입스크립트로 살펴보는 디자인패턴 9] 템플릿 메서드 패턴
템플릿 메서드 패턴(Template Method Pattern)
템플릿 메서드 패턴은 알고리즘의 골격을 정의한다. 템플릿 메서드를 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하면서 특정 단계를 서브클래스에서 재정의할 수도 있다.
템플릿 메서드 패턴은 일련의 단계로 알고리즘을 정의한 메서드이다. 여러 단계 가운데 하나 이상의 단계가 추상 메서드로 정의되며, 그 추상 메서드는 서브클래스에서 구현된다.
더불어, 후크(hook)라는 추상 클래스에서 선언되지만 기본적인 내용만 구성되어있거나 아무 코드도 들어있지 않은 메서드를 선택적으로 오버라이드 하여 원하는 기능을 구현할 수도 있다. 이어지는 예시에서 작성된 음료 제조 시스템에서는 첨가물 여부를 입력받는 용도로 사용되었다.

음료 제조 시스템
기본적인 템플릿 메서드
caffein.beverage.ts
prepareRecipe() 메서드는 readonly 한정자를 붙여서 서브클래스에서 수정할 수 없도록 하였다. readonly 한정자는 속성이나 인덱스 시그니처에만 붙일 수 있기 때문에 속성에 화살표 함수를 할당하였다.
export abstract class CaffeinBeverage {
readonly prepareRecipe = (): void => {
this.boilWater();
this.brew();
this.pourInCup();
this.addCondiments();
};
abstract brew(): void;
abstract addCondiments(): void;
boilWater(): void {
console.log("물 끓이는 중");
}
pourInCup(): void {
console.log("컵에 따르는 중");
}
}
coffee.ts
CaffeinBeverage를 상속받아 추상 메서드를 구현했다.
import { CaffeinBeverage } from "./caffein.beverage";
export class Coffee extends CaffeinBeverage {
brew(): void {
console.log("커피를 우려내는 중");
}
addCondiments(): void {
console.log("설탕과 우유를 추가하는 중");
}
}
beverage.test.drive.ts
음료의 인스턴스를 생성하고, 추상클래스에 정의된 읽기전용 메서드 prepareRecipe()를 호출하면 템플릿으로 엮은 메서드들이 실행되고, 내부에 정의된 콘솔 출력 값들이 출력된다.
import { Coffee } from "./coffee";
import { Tea } from "./tea";
class BeverageTestDrive {
test(): void {
const coffee = new Coffee();
const tea = new Tea();
console.log("커피를 준비 중...");
coffee.prepareRecipe();
console.log("\\n차를 준비 중...");
tea.prepareRecipe();
}
}
new BeverageTestDrive().test();
후크가 추가된 템플릿 메서드
caffein.beverage.ts
customerWantsCondiments() 메서드가 비동기로 구성되기 때문에 템플릿 메서드 및 customerWantsCondiments() 메서드에 async를 붙여줬다.
export abstract class CaffeinBeverage {
readonly prepareRecipe = async (): Promise<void> => {
this.boilWater();
this.brew();
this.pourInCup();
if (await this.customerWantsCondiments()) this.addCondiments();
};
abstract brew(): void;
abstract addCondiments(): void;
boilWater(): void {
console.log("물 끓이는 중");
}
pourInCup(): void {
console.log("컵에 따르는 중");
}
async customerWantsCondiments(): Promise<boolean> {
return true;
}
}
coffee.ts
readline 라이브러리를 활용하여 터미널에서 입력받은 값을 기준으로 첨가물 첨가 여부를 체크했다. readline은 비동기로 동작하므로 관련 메서드들도 비동기로 구성했다.
import { CaffeinBeverage } from "./caffein.beverage";
import * as readline from "readline";
export class Coffee extends CaffeinBeverage {
brew(): void {
console.log("커피를 우려내는 중");
}
addCondiments(): void {
console.log("설탕과 우유를 추가하는 중");
}
async customerWantsCondiments(): Promise<boolean> {
const answer = await this.getUserInput();
return answer.toLowerCase() === "y";
}
private async getUserInput(): Promise<string> {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question("커피에 우유와 설탕을 넣을까요? (y/n): ", (answer: string) => {
rl.close();
resolve(answer);
});
});
}
}
beverage.test.drive.ts
test() 메서드도 async/await를 사용하여 비동기 작업을 처리하도록 구현했다. 출력은 아래와 같으며, 터미널에서 입력을 해야 다음으로 넘어간다.
import { Coffee } from "./coffee";
import { Tea } from "./tea";
class BeverageTestDrive {
async test(): Promise<void> {
const coffee = new Coffee();
const tea = new Tea();
console.log("커피를 준비 중...");
await coffee.prepareRecipe();
console.log("\\n차를 준비 중...");
await tea.prepareRecipe();
}
}
new BeverageTestDrive().test();



디자인 원칙
헐리우드 원칙(Hollywood Principle)
저수준 구성 요소가 시스템에 접속할 수는 있지만, 언제 어떻게 그 구성 요소를 사용할지는 고수준 요소가 결정한다.
앞선 예시 코드에서 CaffeinBeverage는 고수준 구성 요소로, 메서드 구현이 필요한 상황에서만 서브클래스들을 호출한다. 서브클래스는 호출 당하기 전까지 추상 클래스를 직접 호출하지 않는다.
의존성 뒤집기 윈칙과 비교
- 의존성 뒤집기 원칙: 구상 클래스 사용을 줄이고, 추상화된 것을 사용해야 한다.
- 할리우드 원칙: 저수준 구성 요소가 컴퓨테이션에 참여하면서도 저수준 구성 요소와 고수준 계층 간 의존을 없앤다.
참고문서
헤드퍼스트 디자인패턴, 한빛미디어