김코딩

템플릿 메서드 패턴(Template Method Pattern) 본문

디자인 패턴

템플릿 메서드 패턴(Template Method Pattern)

김코딩딩 2026. 1. 14. 12:46

디자인 패턴 시리즈의 열 번째 주제는 템플릿 메서드 패턴(Template Method Pattern)이다.

 

전략 패턴"알고리즘 전체를 교체"하는 거였다면, 템플릿 메서드 패턴"알고리즘의 일부만 변경"하는 것이다.

 

예를 들어, 라면을 끓이는 과정을 생각해보자. 물을 끓이고 → 스프를 넣고 → 면을 넣고 → 끓이는 순서는 똑같다. 하지만 신라면은 매운 스프, 안성탕면은 짠 스프를 넣는다. 전체 흐름은 같지만 일부만 다른 것이다. 템플릿 메서드 패턴은 이처럼 알고리즘의 "뼈대(Template)"는 정의하고, 세부 단계는 하위 클래스에서 구현하는 패턴이다

.

이번 글에서는 템플릿 메서드 패턴무엇인지, 왜 필요한지, 어떻게 구현하는지 알아보자.


템플릿 메서드 패턴의 정의

템플릿 메서드 패턴(Template Method Pattern)알고리즘의 골격을 정의하고, 일부 단계를 서브클래스에서 구현하도록 하는 패턴이다. 알고리즘의 구조는 변경하지 않으면서, 특정 단계만 재정의할 수 있다.

 

핵심 특징:

  1. 알고리즘의 골격을 정의한다
  2. 일부 단계를 서브클래스에 위임한다
  3. 상속을 사용한다
  4. 코드 중복을 제거한다

현실 비유:

  • 라면 끓이기 (전체 과정은 같지만 스프만 다름)
  • 문서 작성 (헤더/본문/푸터 구조는 같지만 내용만 다름)
  • 시험 보기 (입실→시험→퇴실은 같지만 과목만 다름)

왜 템플릿 메서드 패턴이 필요할까?

음료를 만드는 카페 시스템을 개발한다고 가정해보자.

// 문제가 있는 코드

// 커피 만들기
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. 마시멜로를 추가한다");
    }
}

문제점:

  1. 코드 중복: "물을 끓인다", "컵에 따른다"가 반복됨
  2. 유지보수 어려움: 순서 변경 시 모든 클래스 수정 필요
  3. 일관성 없음: 각 클래스가 독립적으로 동작
  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();
    }
}

템플릿 메서드 패턴의 장점

  1. 코드 재사용: 공통 로직을 한 곳에 모음
  2. 일관성: 알고리즘 구조를 강제
  3. 확장 용이: 새로운 구현 추가가 쉬움
  4. 유지보수: 공통 부분만 수정하면 전체 반영
  5. 제어 반전: 프레임워크가 흐름을 제어

템플릿 메서드 패턴의 단점

  1. 상속 의존: 상속을 강제함
  2. 유연성 제한: 알고리즘 구조를 바꿀 수 없음
  3. 리스코프 치환 원칙 위반 가능: 잘못 설계하면 문제 발생
  4. 템플릿 메서드 복잡도: 단계가 많으면 이해하기 어려움

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