
템플릿 메서드 (Template Method) 패턴은 여러 클래스에서 공통으로 사용하는 메서드를 템플릿화 하여 상위 클래스에서 정의하고, 하위 클래스마다 세부 동작 사항을 다르게 구현하는 패턴이다. 변하지 않는 기능(템플릿)는 상위 클래스에 구현하고 자주 변경되며 확장할 기능은 하위 클래스에서 만들도록 하여, 상위의 메소드 실행 동작 순서는 고정하면서 세부 실행 내용은 다양화 될 수 있는 경우에 사용된다.
템플릿 메소드 패턴은 상속을 극대화해서 알고리즘의 뼈대를 맞추는 것에 초점을 둔다. 이미 수많은 프레임워크에서도 많은 부분에 템플릿 메소드 패턴이 적용되어 있고 우리가 사용하고 있다.
디자인 패턴에서의
템플릿은 변하지 않는 것을 의미한다.
실제로 템플릿 메서드는 코드의 중복 제거를 위해 사용하는 리팩토링 기법이기도 하다.

AbstractClass(추상 클래스) : 템플릿 메소드를 구현하고, 템플릿 메소드에서 돌아가는 추상 메소드를 선언한다. 이 추상 메소드는 하위 클래스인 ConcreteClass 역할에 의해 구현된다.ConcreteClass(구현 클래스) : Abstract Class를 상속하고 추상 메소드를 구체적으로 구현한다. ConcreteClass에서 구현한 메소드는 AbstractClass의 템플릿 메소드에서 호출된다.
훅(hook) 메서드는 부모의 템플릿 메서드의 영향이나 순서를 제어하고 싶을때 사용되는 메서드 형태를 말한다. 위의 그림에서 보듯이 템플릿 메서드 내에 실행되는 동작을 step2() 이라는 메서드의 참, 거짓 유무에 따라 다음 스텝을 어떻게 이어나갈지 지정한다. 이를 통해 자식 클래스에서 좀더 유연하게 템플릿 메서드의 알고리즘 로직을 다양화 할 수 있다는 특징이 있다.
훅 메소드는 추상 메소드가 아닌 일반 메서드로 구현하는데, 선택적으로 오버라이드하여 자식 클래스에서 제어하거나 아니면 놔두거나 하기 위해서이다.

abstract class AbstractTemplate {
// 템플릿 메소드 : 메서드 앞에 final 키워드를 붙이면 자식 클래스에서 오버라이딩이 불가능함.
// 자식 클래스에서 상위 템플릿을 오버라이딩해서 자기마음대로 바꾸도록 하는 행위를 원천 봉쇄
public final void templateMethod() {
// 상속하여 구현되면 실행될 메소드들
step1();
step2();
if(hook()) { // 안의 로직을 실행하거나 실행하지 않음
// ...
}
step3();
}
boolean hook() {
return true;
}
// 상속하여 사용할 것이기 때문에 protected 접근제어자 설정
protected abstract void step1();
protected abstract void step2();
protected abstract void step3();
}
class ImplementationA extends AbstractTemplate {
@Override
protected void step1() {}
@Override
protected void step2() {}
@Override
protected void step3() {}
}
class ImplementationB extends AbstractTemplate {
@Override
protected void step1() {}
@Override
protected void step2() {}
@Override
protected void step3() {}
// hook 메소드를 오버라이드 해서 false로 하여 템플릿에서 마지막 로직이 실행되지 않도록 설정
@Override
protected boolean hook() {
return false;
}
}

class Client {
public static void main(String[] args) {
// 1. 템플릿 메서드가 실행할 구현화한 하위 알고리즘 클래스 생성
AbstractTemplate templateA = new ImplementationA();
// 2. 템플릿 실행
templateA.templateMethod();
}
}
실제 유니티에서 자식 오브젝트들의 좌표들을 모두 합하는 스크립트가 아래와 같이 있다고 생각해보자.
using System;
using UnityEngine;
class PositionProcessor {
public Vector3 process(Transform parent) {
Vector3 result = Vector3.zero;
try {
foreach(Transform child in parent) {
result += child.position;
}
} catch (Exception e) {
Debug.LogError(e.Message);
}
return result;
}
}
public class Processor : MonoBehaviour
{
private void Start() {
PositionProcessor positionProcessor = new PositionProcessor();
Vector3 totalPosition = positionProcessor.process(this.transform);
Debug.Log("Total Position: " + totalPosition);
}
}

그런데 만일 모든 좌표들을 뺄셈 연산하는 기능이 필요하다고 해보자. 현재의 PositionProcessor를 MinusPositionProcessor 라는 새로운 클래스를 정의하고 기능을 그대로 가져와 뺄셈으로만 바꿔주었다.
class MinusPositionProcessor {
public Vector3 process(Transform parent) {
Vector3 result = Vector3.zero;
try {
foreach(Transform child in parent) {
result -= child.position;
}
} catch (Exception e) {
Debug.LogError(e.Message);
}
return result;
}
}
public class Processor : MonoBehaviour
{
private void Start() {
MinusPositionProcessor positionProcessor = new MinusPositionProcessor();
Vector3 totalPosition = positionProcessor.process(this.transform);
Debug.Log("Total Position: " + totalPosition);
}
}

하지만 연산 부분이 다를 뿐이지, 부모 오브젝트의 트랜스폼을 넘겨받아 모든 자식을 방분하는 알고리즘 자체는 똑같다. 즉, 코드의 중복이 발생한 것이다.
보통 메서드 중복을 해결하기 위해 상속을 하고 부모 클래스에서 메서드를 정의하면 자식 클래스에서 가져와 사용하면 되지만, 위의 메서드 로직을 보면 한두줄만 코드가 다른 상황이다.
그러면 공통된 부분만 따로 메서드로 빼버리면 되는데, 코드 로직상 오히려 더 복잡해질 것만 같다. 따라서 공통된 부분을 메서드로 뺴는게 아닌, 반대로 다른 부분을 메서드로 빼버리는 발상의 전환을 하면 된다.
abstract class PositionProcessor {
public Vector3 process(Transform parent) {
Vector3 result = Vector3.zero;
try {
result = calculate(parent);
} catch (Exception e) {
Debug.LogError(e.Message);
}
return result;
}
protected abstract Vector3 calculate(Transform parent);
protected abstract Vector3 getResult();
}
이제 각 구현 클래스에서 추상 클래스를 상속하고 추상 메서드를 구현하여 알고리즘 로직을 설정해주면, 추상 클래스에서 미리 정의된 템플릿 메서드의 로직에 따라 각기 다른 알고리즘을 수행하게 된다. 중복되는 코드들은 추상 클래스에 모아두고 다른 부분만 각 클래스들이 구현하도록 하여 가독성을 높이고 코드의 중복은 제거시킨 것이다.
class PlusPositionProcessor : PositionProcessor{
protected override Vector3 calculate(Vector3 result, Transform child) {
return result += child.position;
}
}
class MinusPositionProcessor : PositionProcessor{
protected override Vector3 calculate(Vector3 result, Transform child) {
return result -= child.position;
}
}
public class Processor : MonoBehaviour
{
private void Start() {
PlusPositionProcessor plusPositionProcessor = new PlusPositionProcessor();
Vector3 totalPosition1 = plusPositionProcessor.process(this.transform);
Debug.Log("Total Position: " + totalPosition1);
MinusPositionProcessor minusPositionProcessor = new MinusPositionProcessor();
Vector3 totalPosition2 = minusPositionProcessor.process(this.transform);
Debug.Log("Total Position: " + totalPosition2);
}
}
C#은 java와 달리
extends가 아니라: (콜론)을 사용해서 상속을 받는다. 그리고 오버라이드를 할 때도override키워드를 사용해서 명시해 주어야한다.

위에서도 한 번 언급을 했는데, 할리우드 원칙이란 고수준 모듈(추상 클래스, 인터페이스)에 의존하고 고수준 모듈에서 메소드를 실행하라는 원칙이다.
객체끼리 이상하게 얽히고 설켜, 의존성이 복잡하게 꼬여있는 것을 의존성 부패(dependency rot)라고 부르는데, 할리우드 원칙을 활용하여 의존성 부패를 방지할 수 있다.
즉, 쉽게 말하자면 다형성을 사용해 고수준의 객체 타입에서만 웬만하면 메서드 실행를 하라는 말이다.
class LowerA {
void print(int num) {
Debug.Log(num);
}
int calculate(int n1, int n2) {
return n1 + n2;
}
}
class LowerB {
void echo(int num) {
Debug.Log(num);
}
int operate(int n1, int n2) {
return n1 - n2;
}
}
메서드 구성을 자세히 보니 LowerA와 LowerB 클래스의 print와 echo의 메서드는 이름만 다를 뿐이지, 시그니처와 로직이 완전히 동일하다. 따라서 이 때 지금까지 배웠던 대로 상위(고수준) 클래스로 묶어 코드 중복을 줄일 수 있다.
class Higher {
void print(int num) {
Debug.Log(num);
}
}
class LowerA : Higher{
int calculate(int n1, int n2) {
return n1 + n2;
}
}
class LowerB : Higher{
int operate(int n1, int n2) {
return n1 - n2;
}
}
클래스 간 상속 관계를 형성하면 객체 지향의 다형성을 이용할 수 있다. 하지만 아직 문제가 있는데 메서드 로직이 달라 고수준 클래스에서 묶어주지 못한 calcuate와 operate이다.
만약 이 둘을 실행할 필요가 있다면 어쩔 수 없이 다운 캐스팅을 해야한다.
다운캐스팅? 다운 캐스팅, 업 캐스팅 내용이 생각보다 방대하기 때문에 그냥 단순히 쉽게 정리하자면 부모 클래스가 자식 클래스 타입으로 캐스팅 되는 것이라고 생각하면 된다. 나중에 한 번 업, 다운 캐스팅도 한 번 다뤄보면 좋을듯하다.
다운 캐스팅을 해주면 오류가 없어진다.
public class HollyWood : MonoBehaviour
{
void Start() {
Higher obj = new LowerA();
obj.print(1000);
Debug.Log(((LowerA)obj).calculate(10, 20));
obj = new LowerB();
Debug.Log(((LowerB)obj).operate(10, 20));
}
}

이렇게 하위 클래스 메서드를 실행하려고 다운 캐스팅을 해야하는 번거로움이 있는데, 우리는 템플릿 메서드 패턴을 공부했다!
대부분 템플릿 메서드 패턴을 공부할 때 템플릿 메서드에 국한되어 생각하는데, 템플릿 메서드는 알고리즘의 뼈대일 뿐이고, 템플릿 메서드 패턴의 진짜 핵심은 추상 클래스를 통한 코드 통합 및 고수준 의존 유도에 있다.
abstract class Higher {
public void print(int num) {
Debug.Log(num);
}
public abstract int calculate(int n1, int n2);
}
class LowerA : Higher{
public override int calculate(int n1, int n2) {
return n1 + n2;
}
}
class LowerB : Higher{
public override int calculate(int n1, int n2) {
return n1 - n2;
}
}
public class HollyWood : MonoBehaviour
{
void Start() {
Higher obj = new LowerA();
obj.print(1000);
obj.calculate(10, 20);
obj = new LowerB();
obj.calculate(10, 20);
}
}

이제 그냥 calcuate 함수만 써서! (다운 캐스팅 없이) 하위 클래스의 메서드 로직을 실행할 수 있다. 따라서 정리하자면, 템플릿 메서드는 상속을 통해 중복되는 코드를 상위 클래스로 통일시켜 중복을 제거하고, 메서드 시그니처가 같지만 내부 로직이 자식 클래스마다 다른 부분은 추상 메소드를 통해 상위 클래스에서 다형성으로 메서드를 실행할 수 있도록, 고수준 타입으로 유지하라는 것이다.
다음과 같이 커피와 홍차를 만드는 Coffee와 Tea 클래스가 있다고 해보자. 배운바와 같이 코드 중복이 있고, 이를 템플릿 메서드 패턴으로 리팩토링 해주려고 한다.
class Coffee {
void prepareRecipe() {
boilWater(); // "물 끓이기"
brewCoffeeGrinds(); // "커피 우려내기"
pourInCup(); // "컵에 따르기"
addSugarAndMilk(); // "설탕과 우유를 추가"
}
public void boilWater() {
Debug.Log("물 끓이는 중");
}
public void brewCoffeeGrinds() {
Debug.Log("필터로 커피 우려내는 중");
}
public void pourInCup() {
Debug.Log("컵에 따르는 중");
}
public void addSugarAndMilk() {
Debug.Log("설탕과 우유를 추가하는");
}
}
class Tea {
void prepareRecipe() {
boilWater(); // "물 끓이기"
steepTeaBag(); // "차 우리기"
pourInCup(); // "컵에 따르기"
}
public void boilWater() {
Debug.Log("물 끓이는 중");
}
public void steepTeaBag() {
Debug.Log("차를 우리는 중");
}
public void pourInCup() {
Debug.Log("컵에 따르는 중");
}
}
우선 boilWater()부터 pourInCup() 메서드까지는 Coffee와 Tea 클래스에서 공통적으로 실행되는 부분이니, 추상 메서드로 따로 빼서 묶어서 구현하면 될 것 같다.
그렇다면 Coffee 클래스에만 있는 addSugarAndMilk()는 어떻게 할까? 바로 이곳을 Hook 처리하여 Coffe 자식 클래스에서만 실행되도록 한다.
abstract class CaffeineBeverage {
public void prepareRecipe() {
boilWater();
brew();
pourInCup();
if(customerWantsCondiments()) {
addCondiments();
}
}
protected abstract void brew();
protected abstract void addCondiments();
protected virtual bool customerWantsCondiments() {
return false;
}
public void boilWater() {
Debug.Log("물 끓이는 중");
}
public void pourInCup() {
Debug.Log("컵에 따르는 중");
}
}
class Coffee : CaffeineBeverage {
protected override void brew() {
Debug.Log("필터로 커피 우려내는 중");
}
protected override void addCondiments() {
Debug.Log("설탕과 우유를 추가하는 중");
}
protected override bool customerWantsCondiments() {
Debug.Log("고객한테 설탕과 우유를 추가할 지 물어보는 중");
return true;
}
}
class Tea : CaffeineBeverage {
protected override void brew() {
Debug.Log("컵에 따르는 중");
}
protected override void addCondiments() {}
}
virtual과 abstract 차이 저번에 TIL에서 한 번 다뤘으므로 해당 문서를 참고하면 이해하기 쉽다. 나중에 한 번 더 자세히 문서로 다룰 예정이다.
중간에 hook 메서드 역할인 customerWantsCondiments()의 구현부에 단순 true 리턴하기 전에 Debug.Log를 한 번 더 찍어줬는데, 이런 식으로 hook 메서드를 좀 더 다양하게 응용할 수 있다.
public class Drink : MonoBehaviour
{
void Start() {
CaffeineBeverage coffee = new Coffee();
coffee.prepareRecipe();
CaffeineBeverage tea = new Tea();
tea.prepareRecipe();
}
}

전략 패턴과 템플릿 메서드 패턴은 알고리즘을 때에 따라 적용한다는 컨셉으로, 둘이 공통점을 가지고 있다. 또한 전략 및 템플릿 메서드 패턴은 개방형 폐쇄 원칙을 충족하고 코드를 변경하지 않고 소프트웨어 모듈을 쉽게 확장할 수 있도록 하는데 사용할 수 있다.
전략 패턴은 합성(composition)을 통해 해결책을 강구하면, 템플릿 메서드는 상속(inheritance)을 통해 해결책을 제시한다. 그래서 전략 패턴은 클라이언트와 객체 간의 결합이 느슨한 반면, 템플릿 메서드 패턴에서는 두 모듈이 더 밀접하게 결합된다. (결합도가 올라간다는 뜻이고 결합도 높으면 안 좋다.)
전략 패턴에서는 대부분 인터페이스를 사용하지만, 템플릿 메서드 패턴에서는 주로 추상 클래스나 구체적인 클래스를 사용한다.
전략 패턴에서는 전체 전략 알고리즘을 변경할 수 있지만 템플릿 메서드 패턴에서는 알고리즘의 일부만 변경되고 나머지는 변경되지 않은 상태로 유지된다. (템플릿에 종속)
따라서 단일 상속만을 지원하는 C#에서 상속 제한이 있는 템플릿 메서드 패턴보다는, 다양하게 많은 전략을 implements 할 수 있는 전략 패턴이 많이 사용되기도 한다.