Template Method 패턴: 알고리즘 순서는 고정하고 단계만 바꾸는 법
Template Method 패턴이 알고리즘 골격을 상위 클래스에 고정하고 하위 클래스에 단계를 위임하는 구조와 훅 메서드, Strategy·Callback과의 차이를 정리한다.
2026-08-13 · 최초 발행 2025-10-14
ETL 파이프라인을 하나 더 만들 때마다 검증→로딩→변환→저장이라는 순서 자체를 매번 새로 짜고 있다면, 정작 매번 바뀌는 건 순서가 아니라 각 단계의 구현이라는 뜻이다. Template Method 패턴은 이 순서를 상위 클래스(AbstractClass)에 고정하고, 일부 단계만 추상 메서드나 훅(Hook)으로 열어 하위 클래스(ConcreteClass)가 채우도록 한다. 목표는 일관된 처리 순서를 보장하면서 코드 중복을 줄이고, 변경이 일어날 지점을 명확히 격리하는 것이다.
템플릿 메서드, 프리미티브 오퍼레이션, 훅
전체 알고리즘의 순서를 고정하는 것이 템플릿 메서드다. 재정의를 막기 위해 보통 final로 선언하고, 검증·로깅·트랜잭션 경계 같은 공통 선행·후행 처리를 여기 담는다. 하위 클래스가 반드시 구현해야 하는 필수 단계는 프리미티브 오퍼레이션이라 부르고, 도메인별 차이를 반영하며 템플릿 메서드가 이들을 호출한다. 반면 훅 메서드는 선택적 단계로, 기본 구현이 no-op이거나 기본값을 반환하며 조건부 분기·추가 검증·사후 처리 같은 커스터마이징 지점을 제공한다.
이 구조는 결국 불변 구간과 가변 구간을 가르는 일이다. 순서·트랜잭션·락 정책 같은 불변 규칙은 상위 클래스가 강제하고, 도메인마다 달라지는 가변 규칙은 하위 클래스에 위임해 변경 영향 범위를 최소화한다.
Hollywood Principle: 프레임워크가 흐름을 쥔다
"Don't call us, we'll call you" — 애플리케이션 코드가 흐름을 이끄는 게 아니라 프레임워크가 흐름을 주도하고, 애플리케이션은 훅과 구현만 제공한다는 원칙(Inversion of Control)이 이 패턴에 그대로 반영돼 있다. 하위 클래스는 언제 자신의 메서드가 호출될지 알 필요가 없다 — 상위 클래스의 템플릿 메서드가 그 시점을 결정한다.
역할 분담: AbstractClass와 ConcreteClass
처리 순서와 오류 처리
입력이 들어오면 검증(validate)을 거치고, 실패하면 예외를 던지고 로그를 남긴 뒤 중단한다. 로드(load) 단계에서 외부 자원 접근이 실패하면 재시도나 폴백 정책을 적용하고, 변환(transform)에서 데이터를 정규화하고 비즈니스 규칙을 적용한 뒤, 렌더(render)에서 결과를 생성·저장하며 I/O 예외를 처리한다. 마지막 사후 처리(afterProcess) 훅에서 메트릭·알림·캐시 무효화를 수행한다. 트랜잭션 경계는 템플릿 메서드에 위치시켜 단계별 예외를 상위에서 캡처하고 롤백·정리를 보장한다.
CSV 익스포터로 보는 구현
JDK 17 이상, 표준 javac/java 사용을 전제로 한다.
// File: TemplateMethodDemo.java
abstract class DataExporter {
public final void export(String inputPath, String outputPath) {
long start = System.currentTimeMillis();
validate(inputPath, outputPath);
Object raw = load(inputPath);
Object model = transform(raw);
write(outputPath, model);
afterExport(model); // hook
System.out.println("[OK] Elapsed ms=" + (System.currentTimeMillis() - start));
}
protected void validate(String in, String out) {
if (in == null || out == null || in.isBlank() || out.isBlank())
throw new IllegalArgumentException("Invalid path");
}
protected abstract Object load(String inputPath);
protected abstract Object transform(Object raw);
protected abstract void write(String outputPath, Object model);
// Hook: optional
protected void afterExport(Object model) {
// no-op by default
}
}
class CsvExporter extends DataExporter {
@Override
protected Object load(String inputPath) {
// mock: read CSV lines
return java.util.List.of("a,1", "b,2", "c,3");
}
@Override
protected Object transform(Object raw) {
java.util.List<String> lines = (java.util.List<String>) raw;
return lines.stream()
.map(l -> l.split(","))
.map(arr -> new String[]{arr[0].toUpperCase(), arr[1]})
.toList();
}
@Override
protected void write(String outputPath, Object model) {
@SuppressWarnings("unchecked")
java.util.List<String[]> rows = (java.util.List<String[]>) model;
StringBuilder sb = new StringBuilder();
rows.forEach(arr -> sb.append(arr[0]).append(':').append(arr[1]).append('\n'));
// mock write to console instead of file
System.out.println("== CSV Export ==");
System.out.print(sb);
}
@Override
protected void afterExport(Object model) {
System.out.println("Post-metrics: rows=" + ((java.util.List<?>) model).size());
}
}
public class TemplateMethodDemo {
public static void main(String[] args) {
DataExporter exporter = new CsvExporter();
exporter.export("input.csv", "out.txt");
}
}
컴파일·실행은 javac TemplateMethodDemo.java 다음 java TemplateMethodDemo다.
공통 골격이 필요한 실무 사례
- 배치 파이프라인/ETL — 공통 단계(검증→로딩→변환→저장)를 고정하고 데이터 소스별로 하위 클래스를 나누며, 데이터 품질 규칙·재처리 정책을 일관 적용한다
- 결제/정산 처리 — 공통 승인 흐름을 유지하고 PG사별 서명·검증·에러 맵핑만 바꾸며, 트랜잭션 경계·로깅·리트라이는 템플릿에서 강제한다
- 문서/보고서 생성 — 표준 구성(헤더→본문→푸터)을 고정하고 출력 형식(HTML/PDF/Excel)만 하위에서 정의한다
- 파서/검증기 — 토큰화→구문 분석→의미 검증 순서를 고정하고 문법·규칙만 치환한다
- 게임 AI 턴 처리 — 입력 수집→의사결정→행동 실행→상태 업데이트를 고정하고 캐릭터·난이도별 전략만 차등한다
Template Method, Strategy, Callback 비교
| 항목 | Template Method | Strategy | Callback/Hook |
|---|---|---|---|
| 확장성 | 상속 기반, 단계 단위 확장 | 합성 기반, 알고리즘 교체 | 미세 조정, 포인트 확장 |
| 런타임 교체 | 낮음(클래스 교체 필요) | 높음(객체 교체) | 중간(리스너 등록/해제) |
| 일관된 순서 보장 | 매우 높음 | 전략에 따라 상이 | 훅 포인트 한정 |
| 복잡도 | 중간(계층 설계 필요) | 중간(인터페이스 분리) | 낮음~중간 |
| 오버헤드 | 낮음 | 낮음 | 낮음 |
도입 전 확인할 것들
템플릿 메서드는 final로 선언하고 로그·트랜잭션·리소스 관리 같은 공통 정책은 상위에 고정하는 것이 기본이다. 추상 메서드는 최소한으로 두고 훅은 기본이 no-op이 되게 하며, 템플릿의 불변 조건과 실행 순서를 문서화하고 실패 시 일괄 처리·롤백을 보장해야 한다.
다만 상속 결합도가 올라가면서 취약한 기반 클래스 문제가 생길 수 있어, 변형 폭이 큰 영역이라면 Strategy나 Policy로 합성 전환하는 편이 낫다. 하위 클래스가 지나치게 늘어날 위험도 있으므로 변형 축의 기준을 명확히 하고 팩토리나 DI로 관리해야 하며, 병렬 처리나 상태 공유가 있다면 스레드 안전성을 고려해 템플릿 자체는 무상태로 설계하는 것이 기본이다.
도입 전에는 알고리즘 순서가 안정적이고 단계 구현만 자주 바뀌는지, 공통 단계를 상위에서 강제해야 할 규제·보안·운영 표준이 있는지, 변형 케이스 수와 상속 트리 복잡도가 감당할 만한지, Strategy·파이프라인·미들웨어 필터 같은 대안과 비교했을 때도 이 구조가 맞는지를 확인하는 것이 좋다. 원본 사례가 제시하는 정량 효과로는 코드 중복 2040% 감소, 신규 변형 추가 시 평균 개발 공수 30% 절감, 공통 흐름 회귀 결함 1525% 감소, 로깅·계측 누락 50% 이상 감소가 있으며, 그 외에 변경 영향 범위의 예측 가능성이 오르고 온보딩·코드리뷰 효율이 늘며 리트라이·타임아웃·트랜잭션 같은 표준 운영 정책을 강제 적용하기 쉬워진다는 정성적 효과도 함께 제시된다.