템플릿 메서드 패턴 Java 구현 (Template Method in Java)
한눈에 보기
final 템플릿 메서드와 protected abstract 단계로 추상 클래스를 견고하게 설계하는 방법을 다룬다. HttpServlet 등 JDK 사례와 람다 콜백 방식과의 트레이드오프를 비교한다.
최종 업데이트 2026.09.17
Java에서 템플릿 메서드 패턴(Template Method Pattern)을 구현할 때는 추상 클래스의 접근 제어자 설계, final 선언, 훅 메서드(Hook Method) 배치가 품질을 좌우한다. 이 문서는 템플릿 메서드를 Java 코드로 견고하게 설계하는 방법과 JDK, 프레임워크 속 실제 사례를 살펴보고, 상속 대신 람다 콜백으로 같은 목적을 달성하는 현대적 변형과 비교한다.
추상 클래스 설계의 원칙
Java로 템플릿 메서드를 구현할 때 지켜야 할 설계 규칙은 다음과 같다. 첫째, 템플릿 메서드 자체는 public final로 선언한다. final이 없으면 하위 클래스가 골격 전체를 오버라이드해 절차 보장이 무너질 수 있다. 둘째, 하위 클래스가 구현할 단계 메서드는 protected abstract로 선언한다. protected로 두는 이유는 이 메서드들이 외부 클라이언트가 직접 호출할 API가 아니라 상위 클래스만 호출하는 내부 확장 지점이기 때문이다. 셋째, 하위 클래스가 몰라도 되는 공통 단계는 private으로 숨겨 오버라이드 자체를 차단한다.
public abstract class ReportGenerator {
// 골격: public final, 외부에 공개되는 유일한 진입점
public final String generate() {
StringBuilder sb = new StringBuilder();
sb.append(header()); // private 공통 단계
sb.append(body()); // protected abstract 가변 단계
if (includeFooter()) { // 훅 메서드
sb.append(footer());
}
return sb.toString();
}
private String header() { return "== 리포트 ==\n"; }
private String footer() { return "\n== 끝 =="; }
protected abstract String body();
// 훅: 합리적 기본값 제공, 선택적 오버라이드
protected boolean includeFooter() { return true; }
}
public class SalesReport extends ReportGenerator {
@Override
protected String body() {
return "이번 달 매출 집계";
}
@Override
protected boolean includeFooter() {
return false; // 이 리포트만 푸터 생략
}
}
추상 메서드 수는 최소로 유지하는 것이 좋다. 필수 구현 지점이 많을수록 하위 클래스 작성 부담이 커지므로, "반드시 달라야 하는 것"만 abstract로 두고 "달라질 수도 있는 것"은 훅으로 내리는 것이 요령이다.
훅 메서드의 역할과 활용
훅 메서드는 기본 구현을 가진 protected 메서드로, 하위 클래스에 선택적 개입 지점을 열어 준다. 활용 형태는 세 가지다. 첫째, 위 예시의 includeFooter처럼 boolean을 반환해 알고리즘의 조건 분기를 하위 클래스가 제어하게 하는 형태다. 둘째, 특정 단계 전후에 빈 구현으로 끼워 둔 beforeBody, afterBody 같은 확장 지점 형태로, 하위 클래스는 필요할 때만 오버라이드해 로깅이나 자원 준비를 추가한다. 셋째, 기본 동작을 제공하되 교체를 허용하는 형태다. 추상 메서드가 "구현 강제"라면 훅은 "개입 허용"이라는 점이 본질적 차이이며, 이 구분을 문서 주석으로 명시해 주는 것이 협업에 도움이 된다.
JDK와 프레임워크 속 사례
템플릿 메서드는 Java 생태계 어디에나 있다. 컬렉션 프레임워크의 AbstractList는 get과 size만 구현하면 indexOf, contains, iterator 같은 나머지 동작이 골격 코드로 완성되는 골격 구현(skeletal implementation) 클래스다. 서블릿의 HttpServlet은 service 메서드가 HTTP 메서드를 판별해 doGet, doPost로 분기하는 구조로, 개발자는 필요한 do 메서드만 오버라이드한다. 입출력의 InputStream도 추상 메서드 read() 하나를 기반으로 다중 바이트 읽기 메서드들이 구현되어 있다. Spring에서는 JdbcTemplate처럼 이름에 템플릿이 붙은 클래스들이 자원 획득, 예외 변환, 자원 해제라는 골격을 제공하는데, 이들은 상속이 아니라 콜백 객체를 받는 변형이라는 점이 흥미로운 관찰 지점이다.
콜백(람다) 기반 변형과 비교
Java 8 이후에는 상속 없이 같은 목적을 이루는 방법이 널리 쓰인다. 가변 단계를 추상 메서드 대신 함수형 인터페이스 파라미터로 받는 것이다.
public class ReportTemplate {
// 가변 단계를 파라미터로 주입받는다
public String generate(Supplier<String> bodySupplier) {
return "== 리포트 ==\n"
+ bodySupplier.get()
+ "\n== 끝 ==";
}
}
// 사용: 하위 클래스 없이 람다로 단계 주입
ReportTemplate template = new ReportTemplate();
String report = template.generate(() -> "이번 달 매출 집계");
이 방식은 사실상 전략 패턴과의 결합으로, Spring의 JdbcTemplate이나 TransactionTemplate이 취한 길이다. 두 접근의 트레이드오프를 정리하면 다음과 같다.
| 기준 | 상속 기반 템플릿 메서드 | 콜백(람다) 기반 변형 |
|---|---|---|
| 가변 단계 전달 | 하위 클래스의 오버라이드 | 함수형 인터페이스 인자 |
| 교체 시점 | 컴파일 타임 고정 | 호출마다 다르게 가능 |
| 상속 자원 | 단일 상속을 소모한다 | 소모하지 않는다 |
| 여러 단계 공유 상태 | 필드로 자연스럽게 공유 | 인자 전달이 번거로울 수 있다 |
| 적합한 상황 | 단계가 많고 긴밀히 얽힌 경우 | 가변 지점이 한두 개인 경우 |
가변 지점이 한두 개라면 콜백 방식이 간결하고 유연하다. 반면 단계가 많고 단계들이 protected 필드나 중간 상태를 공유해야 한다면 상속 기반이 여전히 자연스럽다. 실무에서는 골격 구현 클래스와 인터페이스의 default 메서드, 콜백을 상황에 맞게 섞어 쓴다.
구현 시 흔한 실수
자주 보이는 실수는 네 가지다. 템플릿 메서드에 final을 빠뜨려 골격이 오버라이드되는 것, 단계 메서드를 public으로 열어 외부에서 단계를 직접 호출하게 만드는 것, 추상 메서드를 과도하게 늘려 하위 클래스 작성이 고통스러워지는 것, 그리고 상위 클래스 생성자에서 오버라이드 가능한 메서드를 호출하는 것이다. 마지막 경우 하위 클래스 필드가 초기화되기 전에 하위 클래스 메서드가 실행되어 NullPointerException 같은 미묘한 버그가 생길 수 있으므로, 생성자에서는 오버라이드 가능한 메서드를 호출하지 않는다는 규칙을 지켜야 한다.
면접에서 이렇게 물어본다
- 템플릿 메서드를 Java로 구현할 때 각 메서드의 접근 제어자(public final, protected abstract, private)를 어떻게 설계하고 그 이유는 무엇인가.
- 훅 메서드와 추상 메서드의 차이를 설명하고, 훅을 쓰는 상황을 예로 들어 보라.
HttpServlet이나AbstractList가 템플릿 메서드 패턴인 이유를 구조적으로 설명해 보라.- 상속 기반 템플릿 메서드와
JdbcTemplate식 콜백 방식의 트레이드오프를 비교해 보라. - 상위 클래스 생성자에서 오버라이드 가능한 메서드를 호출하면 어떤 문제가 생기는가.
디자인 패턴에서 같이 읽기
- 어댑터 패턴 (Adapter Pattern)호환되지 않는 인터페이스를 클라이언트가 기대하는 형태로 변환해 함께 동작하게 만드는 구조 패턴이다. 타깃, 어댑티, 어댑터, 클라이언트의 협력 구조와 장단점, 유사 패턴과의 차이를 정리한다.
- 어댑터 패턴 Java 구현 (Adapter Pattern in Java)InputStreamReader, Arrays.asList 등 JDK 속 어댑터 사례를 살펴보고, 클래스 어댑터와 객체 어댑터 두 구현 변형의 차이와 Java에서 객체 어댑터가 선호되는 이유를 정리한다.
- 컴포지트 패턴 (Composite Pattern)개별 객체와 객체 묶음을 같은 인터페이스로 다루어 트리 구조를 표현하는 구조 패턴이다. 파일과 폴더 예시 코드로 재귀 구조를 설명하고 투명성과 안전성의 트레이드오프를 다룬다.
- 디자인 패턴 개요 (GoF Design Patterns)반복되는 설계 문제의 검증된 해결 구조인 디자인 패턴을 GoF의 생성, 구조, 행위 세 분류로 조망한다. 패턴 선택 기준과 오남용 경계, 면접 대비 학습 전략까지 정리한다.
이 개념, 내 코드에서는 어떻게 물어볼까요
데브핏(DevFit)에 채용공고 URL을 붙여 넣고 레포지토리를 연결하면, 내가 쓴 커밋에서 예상 질문과 어필 포인트를 만듭니다. 하루 10회까지 무료입니다. 합불 판정이나 지원자 랭킹은 만들지 않고, 최종 판단은 사람이 합니다.
공고 넣고 시작하기