| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- GoF 23
- 추상클래스
- 프로그래머스
- Til
- Effective Java
- 배치
- 스케줄러
- 토스
- 프록시 패턴
- 코드카타
- 스프링 배치
- spring boot
- 자바
- 이펙티브 자바
- redis
- 빌더 패턴
- 김영한
- 디자인 패턴
- 계산기
- 트러블슈팅
- DB
- java
- 성능 개선
- lv1
- 템플릿 메서드 패턴
- 로드밸런서
- 백엔드
- 스프링
- Spring
- Spring Batch
- Today
- Total
김코딩
템플릿 메서드 패턴(Template Method Pattern) 본문
디자인 패턴 시리즈의 열 번째 주제는 템플릿 메서드 패턴(Template Method Pattern)이다.
전략 패턴이 "알고리즘 전체를 교체"하는 거였다면, 템플릿 메서드 패턴은 "알고리즘의 일부만 변경"하는 것이다.
예를 들어, 라면을 끓이는 과정을 생각해보자. 물을 끓이고 → 스프를 넣고 → 면을 넣고 → 끓이는 순서는 똑같다. 하지만 신라면은 매운 스프, 안성탕면은 짠 스프를 넣는다. 전체 흐름은 같지만 일부만 다른 것이다. 템플릿 메서드 패턴은 이처럼 알고리즘의 "뼈대(Template)"는 정의하고, 세부 단계는 하위 클래스에서 구현하는 패턴이다
.
이번 글에서는 템플릿 메서드 패턴이 무엇인지, 왜 필요한지, 어떻게 구현하는지 알아보자.
템플릿 메서드 패턴의 정의
템플릿 메서드 패턴(Template Method Pattern)은 알고리즘의 골격을 정의하고, 일부 단계를 서브클래스에서 구현하도록 하는 패턴이다. 알고리즘의 구조는 변경하지 않으면서, 특정 단계만 재정의할 수 있다.
핵심 특징:
- 알고리즘의 골격을 정의한다
- 일부 단계를 서브클래스에 위임한다
- 상속을 사용한다
- 코드 중복을 제거한다
현실 비유:
- 라면 끓이기 (전체 과정은 같지만 스프만 다름)
- 문서 작성 (헤더/본문/푸터 구조는 같지만 내용만 다름)
- 시험 보기 (입실→시험→퇴실은 같지만 과목만 다름)
왜 템플릿 메서드 패턴이 필요할까?
음료를 만드는 카페 시스템을 개발한다고 가정해보자.
// 문제가 있는 코드
// 커피 만들기
public class Coffee {
public void make() {
System.out.println("1. 물을 끓인다");
System.out.println("2. 커피를 우려낸다");
System.out.println("3. 컵에 따른다");
System.out.println("4. 설탕과 우유를 추가한다");
}
}
// 홍차 만들기
public class Tea {
public void make() {
System.out.println("1. 물을 끓인다"); // 중복!
System.out.println("2. 차를 우려낸다");
System.out.println("3. 컵에 따른다"); // 중복!
System.out.println("4. 레몬을 추가한다");
}
// 핫초코 만들기
public class HotChocolate {
public void make() {
System.out.println("1. 물을 끓인다"); // 중복!
System.out.println("2. 초코를 녹인다");
System.out.println("3. 컵에 따른다"); // 중복!
System.out.println("4. 마시멜로를 추가한다");
}
}
문제점:
- 코드 중복: "물을 끓인다", "컵에 따른다"가 반복됨
- 유지보수 어려움: 순서 변경 시 모든 클래스 수정 필요
- 일관성 없음: 각 클래스가 독립적으로 동작
- 확장 어려움: 새로운 음료 추가 시 또 중복 코드 작성
템플릿 메서드 패턴은 "공통 부분은 부모 클래스에, 다른 부분만 자식 클래스에"로 이 문제를 해결한다.
순수 Java로 구현하기
1단계: 추상 클래스 정의 (템플릿 메서드)
// 음료 만들기의 템플릿
public abstract class Beverage {
// 템플릿 메서드: 알고리즘의 골격
public final void make() {
boilWater(); // 공통: 물 끓이기
brew(); // 다름: 서브클래스에서 구현
pourInCup(); // 공통: 컵에 따르기
addCondiments(); // 다름: 서브클래스에서 구현
}
// 공통 메서드
private void boilWater() {
System.out.println("물을 끓인다");
}
private void pourInCup() {
System.out.println("컵에 따른다");
}
// 추상 메서드: 서브클래스가 구현해야 함
protected abstract void brew();
protected abstract void addCondiments();
}
2단계: 구체적인 클래스 구현
// 커피
public class Coffee extends Beverage {
@Override
protected void brew() {
System.out.println("커피를 우려낸다");
}
@Override
protected void addCondiments() {
System.out.println("설탕과 우유를 추가한다");
}
}
// 홍차
public class Tea extends Beverage {
@Override
protected void brew() {
System.out.println("차를 우려낸다");
}
@Override
protected void addCondiments() {
System.out.println("레몬을 추가한다");
}
}
// 핫초코
public class HotChocolate extends Beverage {
@Override
protected void brew() {
System.out.println("초코를 녹인다");
}
@Override
protected void addCondiments() {
System.out.println("마시멜로를 추가한다");
}
}
3단계: 사용 예시
public class Main {
public static void main(String[] args) {
System.out.println("=== 커피 만들기 ===");
Beverage coffee = new Coffee();
coffee.make();
System.out.println("\n=== 홍차 만들기 ===");
Beverage tea = new Tea();
tea.make();
System.out.println("\n=== 핫초코 만들기 ===");
Beverage hotChocolate = new HotChocolate();
hotChocolate.make();
}
}

장점:
- 중복 코드 제거됨
- 알고리즘 구조를 한 곳에서 관리
- 새로운 음료 추가 쉬움
- 일관성 보장
Hook 메서드 (선택적 단계)
템플릿 메서드에 선택적 단계를 추가할 수 있다.
public abstract class Beverage {
// 템플릿 메서드
public final void make() {
boilWater();
brew();
pourInCup();
// Hook: 원하면 추가 재료를 넣을 수 있음
if (wantsCondiments()) {
addCondiments();
}
}
private void boilWater() {
System.out.println("물을 끓인다");
}
private void pourInCup() {
System.out.println("컵에 따른다");
}
protected abstract void brew();
protected abstract void addCondiments();
// Hook 메서드: 기본 구현 제공, 필요시 오버라이드
protected boolean wantsCondiments() {
return true; // 기본값
}
}
// 블랙커피 (추가 재료 없음)
public class BlackCoffee extends Beverage {
@Override
protected void brew() {
System.out.println("커피를 우려낸다");
}
@Override
protected void addCondiments() {
System.out.println("아무것도 추가하지 않는다");
}
@Override
protected boolean wantsCondiments() {
return false; // 추가 재료 안 넣음
}
}
// 사용
public class Main {
public static void main(String[] args) {
System.out.println("=== 블랙커피 만들기 ===");
Beverage blackCoffee = new BlackCoffee();
blackCoffee.make();
}
}
```
**실행 결과:**
```
=== 블랙커피 만들기 ===
물을 끓인다
커피를 우려낸다
컵에 따른다
(추가 재료 단계 생략됨)
실전 예제: 게임 캐릭터
// 게임 캐릭터 템플릿
public abstract class GameCharacter {
// 템플릿 메서드: 게임 플레이 흐름
public final void play() {
initialize();
startPlaying();
if (hasSpecialSkill()) {
useSpecialSkill();
}
endPlaying();
}
// 공통 단계
private void initialize() {
System.out.println("🎮 게임 시작");
}
private void endPlaying() {
System.out.println("🏁 게임 종료\n");
}
// 서브클래스가 구현
protected abstract void startPlaying();
// Hook 메서드
protected boolean hasSpecialSkill() {
return false;
}
protected void useSpecialSkill() {
// 기본 구현 없음
}
}
// 전사
public class Warrior extends GameCharacter {
@Override
protected void startPlaying() {
System.out.println("⚔️ 전사: 근접 공격!");
}
@Override
protected boolean hasSpecialSkill() {
return true;
}
@Override
protected void useSpecialSkill() {
System.out.println("💥 전사 특수기: 광폭화!");
}
}
// 마법사
public class Mage extends GameCharacter {
@Override
protected void startPlaying() {
System.out.println("🔮 마법사: 마법 공격!");
}
@Override
protected boolean hasSpecialSkill() {
return true;
}
@Override
protected void useSpecialSkill() {
System.out.println("✨ 마법사 특수기: 메테오!");
}
}
// 궁수
public class Archer extends GameCharacter {
@Override
protected void startPlaying() {
System.out.println("🏹 궁수: 원거리 공격!");
}
// 특수기 없음 (기본값 사용)
}
// 사용
public class Main {
public static void main(String[] args) {
System.out.println("=== 전사 플레이 ===");
GameCharacter warrior = new Warrior();
warrior.play();
System.out.println("=== 마법사 플레이 ===");
GameCharacter mage = new Mage();
mage.play();
System.out.println("=== 궁수 플레이 ===");
GameCharacter archer = new Archer();
archer.play();
}
}
템플릿 메서드 패턴의 장점
- 코드 재사용: 공통 로직을 한 곳에 모음
- 일관성: 알고리즘 구조를 강제
- 확장 용이: 새로운 구현 추가가 쉬움
- 유지보수: 공통 부분만 수정하면 전체 반영
- 제어 반전: 프레임워크가 흐름을 제어
템플릿 메서드 패턴의 단점
- 상속 의존: 상속을 강제함
- 유연성 제한: 알고리즘 구조를 바꿀 수 없음
- 리스코프 치환 원칙 위반 가능: 잘못 설계하면 문제 발생
- 템플릿 메서드 복잡도: 단계가 많으면 이해하기 어려움
Spring에서의 템플릿 메서드 패턴
Spring의 많은 클래스가 템플릿 메서드 패턴을 사용한다.
JdbcTemplate
// Spring의 JdbcTemplate가 템플릿 메서드 패턴 사용
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public List<User> findAll() {
// JdbcTemplate이 공통 로직 처리:
// 1. Connection 열기
// 2. PreparedStatement 생성
// 3. ResultSet 처리
// 4. Exception 처리
// 5. Connection 닫기
return jdbcTemplate.query(
"SELECT * FROM users",
(rs, rowNum) -> new User( // 개발자는 이 부분만 구현
rs.getLong("id"),
rs.getString("name")
)
);
}
}
RestTemplate
// RestTemplate도 템플릿 메서드 패턴
@Service
public class ApiService {
private final RestTemplate restTemplate;
public String callApi() {
// RestTemplate이 공통 로직 처리:
// 1. HTTP Connection 생성
// 2. Request 전송
// 3. Response 수신
// 4. Exception 처리
// 5. Connection 닫기
return restTemplate.getForObject(
"https://api.example.com/data",
String.class
);
}
}
TransactionTemplate
// TransactionTemplate도 템플릿 메서드 패턴
@Service
public class OrderService {
private final TransactionTemplate transactionTemplate;
public void createOrder() {
transactionTemplate.execute(status -> {
// TransactionTemplate이 공통 로직 처리:
// 1. Transaction 시작
// 2. 비즈니스 로직 실행
// 3. Commit/Rollback
// 개발자는 비즈니스 로직만 작성
saveOrder();
updateStock();
return null;
});
}
}
실무에서 언제 사용할까?
사용하면 좋은 경우:
- 여러 클래스가 비슷한 흐름을 가질 때
- 알고리즘의 일부만 다를 때
- 코드 중복이 많을 때
- 프레임워크 개발 시
사용하지 않아도 되는 경우:
- 알고리즘이 완전히 다를 때
- 상속을 피하고 싶을 때
- 유연한 구조가 필요할 때
핵심 원칙:
"알고리즘의 골격은 같고 일부만 다르다면 템플릿 메서드 패턴을 고려하라"
전략 패턴 vs 템플릿 메서드 패턴
| 구분 | 전략 패턴 | 템플릿 메서드 패턴 |
| 사용 방법 | 합성(Composition) | 상속(Inheritance) |
| 변경 범위 | 알고리즘 전체 교체 | 알고리즘 일부만 변경 |
| 유연성 | 높음 (런타임 변경) | 낮음 (컴파일 시 결정) |
| 클래스 수 | 많음 | 적음 |
| 사용 시점 | 알고리즘이 완전히 다를 때 | 알고리즘 구조는 같을 때 |
예시:
// 전략 패턴: 결제 방법 전체를 바꿈
service.setPaymentStrategy(new CardStrategy()); // 카드 결제
service.setPaymentStrategy(new CashStrategy()); // 현금 결제
// 템플릿 메서드: 음료 만드는 일부만 바꿈
Beverage coffee = new Coffee(); // 커피 우려내기
Beverage tea = new Tea(); // 차 우려내기
// 둘 다 "물 끓이기 → 우리기 → 따르기" 흐름은 동일
결론
템플릿 메서드 패턴은 알고리즘의 골격을 정의하고 일부만 변경할 수 있게 하는 패턴이다.
기억해야 할 포인트:
- 알고리즘의 골격을 정의한다
- 상속을 사용한다
- 코드 중복을 제거한다
- Spring의 핵심 패턴 중 하나
- 전략 패턴보다 덜 유연하지만 간단하다
다음 글에서는 객체 간의 의존성을 느슨하게 만드는 옵저버 패턴을 알아볼 것이다.
다음 글 예고: 옵저버 패턴 - 객체의 상태 변화를 관찰하라
'디자인 패턴' 카테고리의 다른 글
| 옵저버 패턴(Observer Pattern) (1) | 2026.01.14 |
|---|---|
| 전략 패턴(Strategy Pattern) (0) | 2026.01.14 |
| 프록시 패턴(Proxy Pattern) (0) | 2026.01.13 |
| 데코레이터 패턴(Decorator Pattern) (0) | 2026.01.13 |
| 어댑터 패턴(Adapter Pattern) (0) | 2026.01.13 |