본문 바로가기
Programming

디자인패턴 시리즈 6. 템플릿 메소드 패턴 (Template Method Pattern)

by LeeJ1Hyun 2023. 1. 17.

템플릿 메소드 패턴 (Template Method Pattern)

알고리즘의 골격을 정의한다. 자세히 설명하면 알고리즘의 일부 단계를 서브 클래스에서 구현할 수 있으며, 알고리즘의 구조는 그대로 유지하되 특정 단계를 서브 클래스에서 재정의할 수 있다.

 

코드에 적용해보기

커피와 홍차를 만드는 방법은 아주 유사하다. 물을 끓이고, 커피를 우리고, 컵에 따르고, 기호에 따라 설탕 혹은 우유를 첨가한다. 홍차는 물을 끓이고, 찻잎을 우리고, 컵에 따르고, 레몬을 추가한다. 전략 패턴에서도 배웠던 원칙인 "달라지는 부분과 달라지지 않는 부분을 분리한다" 라는 원칙에 따라 분리해보자. 물을 끓이고, 컵에 따르는 과정은 완전히 동일한 행위이다.

 

 

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("컵에 따른다");
	}
}

 

커피와 홍차를 준비하는 과정의 메소드를 담은 prepareRecipe() 메소드를 선언했다. boilWater, pourInCup() 메소드는 두 클래스 모두 동일한 행위를 하고 있으므로 공통된 부분을 베이스 클래스에 추상화하면 더 객체지향적인 코드가 될 것이다. 찻잎을 우리거나 커피를 내리는 행위도 유사하지만 재료만 다르고, 무언가 첨가하는 것도 우유, 설탕 혹은 레몬이 되는 것일뿐 행위 자체는 유사하다. 고로 이 메소드 또한 추상화를 통하여 구현할 수 있다.

 

public abstract class CaffeineBeverage {
  
	final void prepareRecipe() {
		boilWater();
		brew();
		pourInCup();
		addCondiments();
	}
 
	abstract void brew();
  
	abstract void addCondiments();
 
	void boilWater() {
		System.out.println("물을 끓인다");
	}
  
	void pourInCup() {
		System.out.println("컵에 따른다");
	}
}

 

커피와 홍차 모두가 상속 받을 슈퍼 클래스인 CaffeineBeverage안에 brewCoffeeGrinds, steepTeaBag() 메소드를 brew() 메소드로 추상화했고, addSugarAndMilk, addLemon() 메소드는 addCondiments() 메소드로 추상화했다. 상속 받을 때 prepareRecipe() 메소드를 아무렇게나 구현하지 못하도록 final로 선언했다. 템플릿 메소드 패턴의 요지가 알고리즘의 구조는 그대로 유지하되 특정 메소드를 서브 클래스에서 재정의하는 것이기 때문이다.

 

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("레몬을 추가한다");
	}
}

 

CaffeineBeverage 슈퍼 클래스 덕분에 이제 커피와 홍차 클래스에서는 공통된 물 끓이기, 컵에 따르기와 같은 행위를 중복해서 선언할 필요가 없다. 커피, 홍차 클래스에 템플릿 메소드 패턴을 적용했다. CaffeineBeverage 클래스의 prepareRecipe() 메소드가 바로 템플릿 메소드이다. 어떤 일련의 알고리즘의 템플릿 역할을 하기 때문이다. 카페인 음료를 만드는 것의 '틀' 역할을 하고 있다.

 

템플릿 메소드는 알고리즘의 각 단계를 정의하고, 서브 클래스에서 특정 단계를 구현할 수 있도록 유도한다.

 

public class Test {
	public static void main(String[] args) {
 
		Tea tea = new Tea();
        	tea.prepareRecipe();
        
		Coffee coffee = new Coffee();
		coffee.prepareRecipe();
	}
}

 

클라이언트에서는 prepareRecipe() 메소드만으로 홍차와 커피를 만들 수 있다. 만약 녹차가 추가된다고 해도 녹차 클래스를 만들어 CaffeineBeverage 클래스를 상속 받아 구현하면 되기 때문에 문제가 없다. 즉, 수정에는 닫혀있고 확장에 열려있다.

 

템플릿 메소드 패턴을 적용한 홍차와 커피 만들기

 

지금까지 리팩토링한 프로그램의 구조를 보면 위와 같다.

 

템플릿 메소드 패턴의 다이어그램

 

일반적인 템플릿 메소드 패턴의 다이어그램은 AbstractClass에 템플릿 메소드가 들어있다. abstract로 선언된 메소드들이 템플릿 메소드에서 활용되며, 템플릿 메소드는 알고리즘을 구현할 때 primitiveOperation(기본 단계)을 활용한다. 단, 각각의 메소드들의 구체적인 구현에는 관여하지 않는다. ConcreteClass에서 abstract로 선언된 메소드를 구현한다. templateMethod()가 이 메소드들을 사용한다.

 

템플릿 메소드 패턴에 후크(hook)를 더해보자

 

abstract class AbstractClass {
	final void templateMethod() {
    	primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
        hook();
    }
    
    abstract void primitiveOperation1();
    
    abstract void primitiveOperation2();
    
    final void concreteOperation() { // final로 선언되어 서브클래스에서 상속 불가
    	// 구현부
    }
    
    void hook() {}
}

 

기본적인 내용만 구현되어 있거나 아무것도 하지 않는 구상 메소드를 정의할 수 있는데 이러한 메소드를 후크라고 부른다. 서브 클래스에서 상속 받을 수도 있지만 하지 않아도 된다. 서브 클래스는 이제 다양한 위치에서 알고리즘에 끼어들 수 있게 되었다. 어떻게 알고리즘에 끼어드는지 CaffeineBeverage 클래스를 수정해서 보여주겠다.

 

public abstract class CaffeineBeverageWithHook {
 
	final void prepareRecipe() {
		boilWater();
		brew();
		pourInCup();
		if (customerWantsCondiments()) { // customerWantsCondiments() 메소드로 실행 여부 결정
			addCondiments();
		}
	}
 
	abstract void brew();
 
	abstract void addCondiments();
 
	void boilWater() {
		System.out.println("물을 끓인다");
	}
 
	void pourInCup() {
		System.out.println("컵에 따른다");
	}
 
	boolean customerWantsCondiments() { // 서브 클래스에서 필요할 때 상속할 수 있으므로 후크
		return true;
	}
}

 

public class CoffeeWithHook extends CaffeineBeverageWithHook {
 
	public void brew() {
		System.out.println("커피를 우려낸다");
	}
 
	public void addCondiments() {
		System.out.println("설탕, 우유를 추가한다");
	}
 
	public boolean customerWantsCondiments() {

		String answer = getUserInput();

		if (answer.toLowerCase().startsWith("y")) {
			return true;
		} else {
			return false;
		}
	}
 
	private String getUserInput() {
		String answer = null;

		System.out.print("커피에 우유, 설탕을 첨가하시겠습니까 (y/n)? ");

		BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
		try {
			answer = in.readLine();
		} catch (IOException ioe) {
			System.err.println("IO error");
		}
		if (answer == null) {
			return "no";
		}
		return answer;
	}
}

 

후크를 통해서 상황에 따라 알고리즘을 변경할 수 있게 되었다. 하지만 추상메소드를 써도 되지 않나 싶기도 하다. 이를 구분할 수 있는 방법이 있다. 서브 클래스가 알고리즘의 특정 단계를 제공해야 한다면 추상 메소드를, 알고리즘의 특정 단계가 선택적으로 적용된다면 후크를 쓰면 된다. 후크는 반드시 구현해야 하는 메소드가 아니기 때문이다.

 

템플릿 메소드 패턴에서 추상 메소드가 너무 많아지면 서브 클래스에서 일일이 구현해야 하므로 오히려 번거로워진다. 그래서 알고리즘의 단계를 잘게 쪼개는 것을 주의해야 한다. 동시에 너무 큰 단위로 자르면 유연성이 떨어지므로 적절하게 결정해야 한다. 이때 필수가 아닌 부분을 후크로 구현하면 추상 메소드를 만들어야 하는 부담이 줄어든다.

 

먼저 연락하지마, 내가 알아서 연락할게

 

일명 할리우드 원칙이라고 하며, 고수준 구성 요소가 저수준 구성 요소를 언제, 어떻게 사용할지를 결정한다. 저수준 구성 요소도 시스템에 접속할 수는 있다. 그러나 고수준 구성 요소를 직접 호출할 수는 없다. 갑자기 이 디자인 원칙을 언급한 이유는 템플릿 메소드 패턴에서 사용하고 있기 때문이다.

 

CaffeineBeverage 클래스는 알고리즘을 담고 있는 고수준 구성 요소로 메소드 구현이 필요한 상황에서만 서브 클래스를 불러 사용한다. 홍차와 커피 클래스는 저수준 구성 요소호 CaffeineBeverage가 호출할 때만 사용이 된다. CaffeineBeverage를 직접 호출할 수는 없다.

 

DIP와 유사한데 이는 구상 클래스 사용을 줄이고 추상화된 것을 사용해야 한다는 원칙이고, 할리우드 원칙은 저수준 구성 요소가 계산에 차며하면서도 저수준, 고수준 구성 요소 계층 간 의존을 없애도록 구축하는 기법이다. 객체를 분리한다는 공통점을 갖지만 의존성을 지양하는 방법은 DIP가 훨씬 강하다.

 

  • 템플릿 메소드 패턴 : 알고리즘의 어떤 단계를 구현하는 방법을 서브 클래스에서 결정
  • 전략 패턴 : 바뀌는 부분을 캡슐화하고 어떤 부분을 사용할지는 서브 클래스에 위임
  • 팩토리 메소드 패턴 : 구상 클래스의 인스턴스 생성을 서브 클래스에서 결정

 

언뜻 보면 템플릿 메소드 패턴과 전략 패턴이 유사하다. 다만 목표를 보면 차이점을 알 수 있다. 템플릿 메소드 패턴은 알고리즘의 일련의 과정들을 나열하고 정의하고 특정 작업은 서브 클래스에서 처리한다. 전략 패턴에서는 슈퍼 클래스의 어떤 것에도 의존하지 않고 알고리즘을 전부 알아서 구현할 수 있다.

 

 

 

 

 

* 아래의 자료들을 참고하였습니다.

에릭 프리먼 · 엘리자베스 롭슨 · 케이시 시에라 · 버트 베이츠, 『헤드퍼스트 디자인패턴 개정판』, 서환수 옮김, 한빛미디어(2022), p312-348.

댓글