템플릿 메소드 (template method)
알고리즘의 골격을 상위클래스에 정의하는 것으로, 일부 단계만을 서브클래스에서 재정의하도록 강제합니다.
패턴명 그대로 템플릿(틀)
을 만들어서 해당 틀을 받아서 필요부분만 수정하는 것입니다.
즉, 정의는 선조가 하고 구현은 후손이 하는 것을 의미하며, 알고리즘은 선조에 구현되어있기 때문에 후손이 어떻게 구현을 하더라도 알고리즘의 순서는 동일하게 실행시킬 수 있습니다.
간단히 코드로 확인해보겠습니다.
추상화 전
커피와 홍차를 제공하는 카페에서 메뉴얼에 따라 커피와 티를 만드는 클래스를 생성해보겠습니다.
public class Coffee {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void brewCoffeeGrinds() {
System.out.println("필터로 커피를 우려내는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
public void addSugarAndMilk() {
System.out.println("설탕과 우유를 추가하는 중");
}
}
public class Tea {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void steepTeaBag() {
System.out.println("찻잎을 우려내는 중");
}
public void addLemon() {
System.out.println("레몬을 추가하는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
}
추상화 후
커피와 티 모두 물을끓이고 컵으 따르는 동작이 같으므로 추상클래스로 분리힌다.
그리고 자세히 보면 찻잎을 우리는 것과 커피를 우리는 것은 ‘우리는 것’이라는 공통점이 있으므로 brew
라는 추상 메소드로 분리해준다. 또한 설탕과 우유를 추가하는 것과 레몬을 추가하는 것은 ‘추가하는 것’이라는 공통점이 있으므로 addCondiments
라는 추상 메소드로 분리해준다.
그러고나면 음료를 만드는 과정인 prepareRecipe
도 공통 항목으로 분리되어 추상클래스에 구현할 수 있다.
레시피의 순서는 변경되면 안되므로 final 메소드로 선언하여 재정의가 불가능하도록 선언한다.
// 메뉴얼 추상클래스
public abstract class CaffeineBeverage {
// 추상클래스에 final로 선언할 경우 재정의(오버라이드)가 불가능하다.
// 이 클래스는 재정의를 막기 위해 final로 선언하였습니다.
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
abstract void brew();
abstract void addCondiments();
public void boilWater() {
System.out.println("물 끓이는 중");
}
public void pourInCup() {
System.out.println("컵에 따르는 중");
}
}
그럼 왜 이 CaffeineBeverage가 템플릿메소드 패턴이 될 수 있는지 확인해보자.
템플릿 메소드 패턴은 알고리즘의 각 단계를 정의하며, 서브 클래스에서 그 단계 중 일부를 구현할 수 있도록 유도하는 것입니다. 위 추상 클래스는 prepareRecipe
라는 메소드로 전체 레시피의 흐름을 관리하며, 공통된 boilWater, pourInCup 메소드는 구현되고 brew, addCondiments 메소드는 추상 메소드로 선언하여 이 추상클래스를 상속받는 서브 클래스가 직접 구현하도록 강제합니다.
public class Coffee extends CaffeineBeverage {
public void brew() {
System.out.println("필터로 커피를 우려내는 중");
}
public void addCondiments() {
System.out.println("설탕과 우유를 추가하는 중");
}
}
public class Tea extends CaffeineBeverage {
public void brew() {
System.out.println("찻잎을 우려내는 중");
}
public void addCondiments() {
System.out.println("레몬을 추가하는 중");
}
}
템플릿 메소드의 장점
- 전체적인 흐름을 관리하는 메소드가 추상클래스 한 곳에만 있으므로 수정이 있을 시 한 곳만 수정하면 됩니다.
- 다른 음료를 추가하더라도 공통된 부분에 대한 중복 코드를 방지할 수 있으며, 일부만 필요에 따라 구현할 수 있다는 간편성이 생깁니다. (캡슐화)
템플릿 메소드 후크 사용해보기
그럼 다음으로 템플릿 메소드 속의 후크(hook)를 알아봅시다.
후크(hook)는 추상 클래스에 선언되어 있지만 기보적인 내용만 구현되어 있거나 아무 코드도 구현되어 있지 않은 메소드입니다.
즉, 서브클래스에서 특정 단계가 선택적으로 적용된다면 후크를 사용하면 됩니다.
public abstract class CaffeineBeverage {
final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if(isCustomerWantsCondiments()) { // 추가 여부에 따라 추가 작업 진행
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
void boilWater() {
System.out.println("물 끓이는 중");
}
void pourInCup() {
System.out.println("컵에 따르는 중");
}
// 별 작업이 없고, 서브클래스에서 필요할 때 오버라이드 할 수 있는 메소드 => 후크
boolean isCustomerWantsCondiments() {
return true;
}
}
import java.io.IOException;
public class Coffee extends CaffeineBeverage {
private static final String QUESTION_ADD_CONDIMENTS = "커피에 우유와 설탕을 넣을까요? (y/n)";
public void brew() {
System.out.println("필터로 커피를 우려내는 중");
}
public void addCondiments() {
System.out.println("설탕과 우유를 추가하는 중");
}
// 추가 여부를 물어보는 작업을 구현 - 후크 메소드를 재정의함
public boolean isCustomerWantsCondiments() {
try {
return AskService.getAnswerYesOrNoByQuestion(QUESTION_ADD_CONDIMENTS);
} catch(IOException e) {
return false;
}
}
}
package com.sec10.TemplateMethodTest;
public class Tea extends CaffeineBeverage {
public void brew() {
System.out.println("찻잎을 우려내는 중");
}
public void addCondiments() {
System.out.println("레몬을 추가하는 중");
}
// 후크 메소드 정의 없이 그냥 진행 => 추상 클래스에서 무조건 true를 반환하므로
// addCondiments가 무조건 실행됨
}
실행결과를 확인해보겠습니다.
public class Main {
public static void main(String[] args) {
CaffeineBeverage coffee = new Coffee();
CaffeineBeverage tea = new Tea();
coffee.prepareRecipe();
System.out.println("===============================");
tea.prepareRecipe();
}
}
이처럼 템플릿 메소드는 정해진 틀에 맞게 알고리즘이 실행되고, 그 중 일부만 구현을 강제하여 중복되는 구현을 제거할 수 있습니다.
참고자료: 헤드퍼스트 디자인패턴 도서