알고리즘의 골격을 정의하다
음료를 만들 때를 생각해 보죠. 커피를 끓이든, 차를 우리든 기본적인 과정은 비슷합니다. 물을 끓이고, 재료를 우리고, 컵에 따르고, 필요에 따라 부가재료를 넣습니다. 하지만 세부적인 방법은 다릅니다. 커피는 원두를 갈아서 우리지만, 차는 잎을 우립니다. 이처럼 음료를 만드는 전체적인 방법은 동일하지만 세부 단계는 다릅니다.
여행 계획도 목적지 선정, 일정 계획, 예약, 짐 준비, 출발이라는 전체적인 방법은 동일하지만, 해외여행인지, 국내여행인지, 캠핑인지에 따라 세부적인 부분은 다릅니다.
이렇듯 특정한 틀, 양식을 템플릿(Template)이라고 합니다. 템플릿은 14세기 중세 프랑스어 templet에서 왔고 목수들이 쓰는 틀, 측정 기준 도구를 의미했습니다. 즉, 무언가를 측정하거나 복제하기 위해 정해 놓은 기준이라는 개념에서 출발했습니다.
음료를 만드는 과정이나 여행 계획, 운동을 하는 단계 또는 문서의 틀, 형식을 템플릿이라고 부르고, 프로그래밍에서도 마찬가지로 로직의 틀을 만들고 세부적인 것은 재정의하는 방식을 템플릿 메서도 패턴이라고 합니다.
다시 말해, 템플릿 메서드 패턴은 알고리즘의 구조를 상위 클래스에서 정의하고, 하위 클래스에서 특정 단계를 구현하도록 하는 행동 디자인 패턴입니다. 이 패턴을 사용하면 알고리즘의 구조는 그대로 유지하면서 알고리즘의 특정 단계만 하위 클래스에서 재정의할 수 있습니다.
1960년대 후반, 최초의 객체지향 언어인 Simula 67에 상속과 가상 메서드의 개념이 도입되었습니다. 추상 클래스에서 구체 메서드와 추상 메서드를 조합하는 아이디어가 여기서부터 시작됩니다. 1970년대 Smalltalk 언어에서 상속 계층과 메서드 오버라이딩(재정의)을 통한 코드 재사용 패턴이 널리 사용되었습니다.
특히 Smalltalk의 컬렉션 클래스들에서 템플릿 메서드와 유사한 패턴이 활용되었습니다. 1980년대 C++언어에서는 가상 함수(virtual function)를 통해 템플릿 메서드 패턴의 구현이 더욱 명확해집니다.
1980년대 후반 애플리케이션 프레임워크들이 등장하면서, 프레임워크가 전체 흐름을 제어하고 사용자가 특정 부분만 구현하는 "제어의 역전(Inversion of Control)" 개념이 발전됩니다. 1994년 GoF는 이러한 다양한 소스에서 나온 실무 경험과 이론적 배경을 종합하여 '템플릿 메서드 패턴'이라는 이름으로 공식화했습니다.
커피와 차를 만드는 과정을 템플릿 메서드 패턴으로 구현된 예제를 보겠습니다.
// 추상 클래스 - 템플릿 메서드를 포함
abstract class CaffeineBeverage {
// 템플릿 메서드 - 알고리즘의 골격을 정의
// final을 사용하여 하위 클래스에서 오버라이드(재정의) 방지
public final void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
// 구체 메서드 - 공통 단계
private void boilWater() {
System.out.println("물을 끓입니다");
}
private void pourInCup() {
System.out.println("컵에 따릅니다");
}
// 추상 메서드 - 하위 클래스에서 반드시 구현해야 함
protected abstract void brew();
protected abstract void addCondiments();
// 훅 메서드 - 하위 클래스에서 선택적으로 오버라이드
protected boolean customerWantsCondiments() {
return true; // 기본값은 true
}
}
// 구체 클래스 1 - 커피
class Coffee extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("필터를 통해 커피를 우려냅니다");
}
@Override
protected void addCondiments() {
System.out.println("설탕과 우유를 추가합니다");
}
// 훅 메서드 오버라이드 - 사용자 입력을 받아 결정
@Override
protected boolean customerWantsCondiments() {
String answer = getUserInput();
return answer.toLowerCase().startsWith("y");
}
private String getUserInput() {
System.out.print("커피에 우유와 설탕을 넣을까요? (y/n): ");
// 실제로는 Scanner 등을 사용하여 입력받음
// 여기서는 시뮬레이션을 위해 고정값 사용
return "y"; // 예시
}
}
// 구체 클래스 2 - 차
class Tea extends CaffeineBeverage {
@Override
protected void brew() {
System.out.println("차잎을 우려냅니다");
}
@Override
protected void addCondiments() {
System.out.println("레몬을 추가합니다");
}
@Override
protected boolean customerWantsCondiments() {
String answer = getUserInput();
return answer.toLowerCase().startsWith("y");
}
private String getUserInput() {
System.out.print("차에 레몬을 넣을까요? (y/n): ");
return "n"; // 예시
}
}
위의 정의된 템플릿 메서드 객체를 사용한 예시입니다.
public class BeverageTest {
public static void main(String[] args) {
System.out.println("=== 커피 제조 ===");
Coffee coffee = new Coffee();
coffee.prepareRecipe();
System.out.println("\n=== 차 제조 ===");
Tea tea = new Tea();
tea.prepareRecipe();
}
}
실행 결과는.
=== 커피 제조 ===
물을 끓입니다
필터를 통해 커피를 우려냅니다
컵에 따릅니다
커피에 우유와 설탕을 넣을까요? (y/n): 설탕과 우유를 추가합니다
=== 차 제조 ===
물을 끓입니다
차잎을 우려냅니다
컵에 따릅니다
차에 레몬을 넣을까요? (y/n):
CaffeineBeverage라는 추상 템플릿을 만들고 커피와 차가 이를 상속받아 세부적인 것을 정의합니다. 객체를 만들 때는 각각 만들지만, 음료를 만드는 과정을 실행하는 prepareRecipe()는 동일하게 작동합니다.
템플릿 메서드 패턴은 알고리즘의 구조를 보존하면서도 유연성을 제공하는 강력한 패턴입니다. 특히 프레임워크 설계나 공통 처리 로직이 있는 시스템에서 유용합니다. 앞서 예를 들었듯이 음료 조제 과정, 운동 순서, 여행 계획, 학습 과정, 의료 과정, 쇼핑 등 실생활에서 템플릿 패턴을 심심치 않게 찾을 수 있습니다.
일정도 목적지도 없이 무작정 여행을 떠나서 가면서 짐을 챙기고, 발길 닿는 대로 갔다가 해가 지면 아무 데서나 자고, 집에 가고 싶으면 그냥 가는 무계획적인 여행도 나름의 낭만이 있어 보입니다. 갑작스럽게 즉석에서 결정하는 것에서 신선함을 느낄 수도 있지만, 안전하지 않고 효율성에서는 떨어집니다.
템플릿은 같은 것을 반복하지 않고, 설계라는 안전한 틀 안에서 일관성과 효율성을 보장합니다. 또한 진정한 창조성과 혁신은 무질서한 자유에서가 아니라 제약과 원칙 위에서 발현됩니다. 마치 재즈 연주자가 기본 화성 진행(템플릿) 위에서 자유로운 즉흥연주(구현)를 통해 아름다운 음악을 만들어내는 것처럼, 견고한 기본 원칙 위에서 창의성이 구현될 때 가장 좋은 결과를 만들 수 있습니다.
스티븐 R. 코비의 『원칙 중심의 리더십』(1991)에서 "원칙(principles)"과 "가치(values)"를 구분한 것처럼, 원칙은 보편적이고 변하지 않는 것이며, 여기에 개인이나 문화에 따라 달라질 수 있는 가치가 더해집니다. 템플릿 메서드 패턴 역시 이와 같은 맥락에서 불변의 구조적 원칙과 가변적인 구현의 조화를 통해 견고하면서도 유연한 시스템을 만들어내는 것입니다.