Template Method Pattern 정리

테사벨로그·2025년 10월 23일

Design Pattern

목록 보기
10/19

1. 왜 Template Method Pattern이 생겨났는가?

문제 상황

// ❌ 나쁜 예: 중복 코드가 많은 Coffee와 Tea 클래스
public class Coffee {
    void prepareRecipe() {
        boilWater();
        brewCoffeeGrinds();
        pourInCup();
        addSugarAndMilk();
    }
    public void boilWater() {
        System.out.println("Boiling water");
    }
    public void brewCoffeeGrinds() {
        System.out.println("Dripping Coffee through filter");
    }
    public void pourInCup() {
        System.out.println("Pouring into cup");
    }
    public void addSugarAndMilk() {
        System.out.println("Adding Sugar and Milk");
    }
}

public class Tea {
    void prepareRecipe() {
        boilWater();
        steepTeaBag();
        pourInCup();
        addLemon();
    }
    // boilWater(), pourInCup() 메서드가 Coffee와 중복!
}

문제점:

  • 코드가 여러 클래스에 중복됨 (boilWater, pourInCup)
  • 알고리즘 변경 시 여러 곳을 수정해야 함
  • 새로운 음료 추가 시 중복 코드 증가
  • 알고리즘의 구조가 각 클래스에 분산되어 관리 어려움

2. Abstract Class VS Interface 선택

왜 Template Method는 Abstract Class를 사용하는가?

Abstract Class를 사용하는 이유:

  1. 공통 구현 코드 공유

    • boilWater(), pourInCup() 같은 메서드는 모든 음료에서 동일
    • 이런 공통 메서드를 부모 클래스에서 구현하여 재사용
  2. 알고리즘 골격 보호

    • prepareRecipe()를 final로 선언하여 알고리즘 순서 고정
    • 서브클래스가 알고리즘 구조를 변경하지 못하도록 제어
  3. 부분적 구현 강제

    • 추상 메서드(brew, addCondiments)로 서브클래스가 반드시 구현해야 할 부분 지정
    • 구현된 메서드와 추상 메서드를 함께 가질 수 있음
// ✅ Template Method는 Abstract Class 사용
public abstract class CaffeineBeverage {
    // final: 알고리즘 골격 보호
    final void prepareRecipe() {
        boilWater();      // 구현됨 (공통)
        brew();            // 추상 메서드 (다름)
        pourInCup();       // 구현됨 (공통)
        addCondiments();   // 추상 메서드 (다름)
    }
    
    // 공통 구현
    void boilWater() {
        System.out.println("Boiling water");
    }
    
    void pourInCup() {
        System.out.println("Pouring into cup");
    }
    
    // 서브클래스가 구현해야 할 부분
    abstract void brew();
    abstract void addCondiments();
}

Interface가 적합하지 않은 이유:

  • Interface는 공통 구현 코드를 가질 수 없음 (Java 8 이전)
  • 알고리즘의 골격을 정의하고 보호할 수 없음
  • 템플릿 메서드를 final로 선언할 수 없음

3. Template Method Pattern 핵심 구조

         AbstractClass (추상 클래스)
      ┌─────────────────────────────┐
      │ final templateMethod() {    │ ← 알고리즘 골격 (final)
      │   concreteOperation1()      │
      │   primitiveOperation1()     │
      │   concreteOperation2()      │
      │   primitiveOperation2()     │
      │   hook()                    │
      │ }                           │
      │                             │
      │ concreteOperation1() { }    │ ← 구현된 메서드
      │ concreteOperation2() { }    │
      │                             │
      │ abstract primitiveOperation1() │ ← 추상 메서드
      │ abstract primitiveOperation2() │
      │                             │
      │ hook() { }                  │ ← 훅 메서드 (선택)
      └─────────────────────────────┘
                    △
                    │ 상속
                    │
         ┌──────────┴──────────┐
         │                     │
    ConcreteClass1      ConcreteClass2
    구현1                구현2

핵심 요소:

  • Template Method: 알고리즘의 골격을 정의 (final)
  • Concrete Methods: 공통으로 구현된 메서드
  • Abstract Methods: 서브클래스가 반드시 구현해야 하는 메서드
  • Hook Methods: 선택적으로 오버라이드 가능한 메서드

4. 예시 코드

Step 1: 추상 클래스 정의

// Template Method Pattern 적용
public abstract class CaffeineBeverage {
    
    // 템플릿 메서드: 알고리즘의 골격 정의
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    
    // 추상 메서드: 서브클래스가 구현
    abstract void brew();
    abstract void addCondiments();
    
    // 공통 메서드: 모든 서브클래스에서 동일
    void boilWater() {
        System.out.println("물 끓이는 중");
    }
    
    void pourInCup() {
        System.out.println("컵에 따르는 중");
    }
}

Step 2: 구체 클래스 구현

// Coffee 구현
public class Coffee extends CaffeineBeverage {
    
    @Override
    void brew() {
        System.out.println("필터로 커피 우려내는 중");
    }
    
    @Override
    void addCondiments() {
        System.out.println("설탕과 우유 추가 중");
    }
}

// Tea 구현
public class Tea extends CaffeineBeverage {
    
    @Override
    void brew() {
        System.out.println("차를 우려내는 중");
    }
    
    @Override
    void addCondiments() {
        System.out.println("레몬 추가 중");
    }
}

Step 3: 사용 예시

public class BeverageTest {
    public static void main(String[] args) {
        System.out.println("=== 커피 만들기 ===");
        CaffeineBeverage coffee = new Coffee();
        coffee.prepareRecipe();
        
        System.out.println("\n=== 차 만들기 ===");
        CaffeineBeverage tea = new Tea();
        tea.prepareRecipe();
    }
}

출력 결과

=== 커피 만들기 ===
물 끓이는 중
필터로 커피 우려내는 중
컵에 따르는 중
설탕과 우유 추가 중

=== 차 만들기 ===
물 끓이는 중
차를 우려내는 중
컵에 따르는 중
레몬 추가 중

5. Hook Method (훅 메서드)

Hook이란?

  • 추상 클래스에서 선언되지만 기본 구현만 제공하거나 비어있는 메서드
  • 서브클래스가 선택적으로 오버라이드할 수 있음
  • 알고리즘의 특정 지점에서 서브클래스가 끼어들 수 있는 기회 제공

Hook 적용 예시

public abstract class CaffeineBeverage {
    
    final void prepareRecipe() {
        boilWater();
        brew();
        pourInCup();
        // Hook 메서드를 이용한 조건부 실행
        if (customerWantsCondiments()) {
            addCondiments();
        }
    }
    
    abstract void brew();
    abstract void addCondiments();
    
    void boilWater() {
        System.out.println("물 끓이는 중");
    }
    
    void pourInCup() {
        System.out.println("컵에 따르는 중");
    }
    
    // Hook 메서드: 기본 구현 제공
    boolean customerWantsCondiments() {
        return true;  // 기본값
    }
}

Hook 오버라이드

import java.io.*;

public class CoffeeWithHook extends CaffeineBeverage {
    
    @Override
    void brew() {
        System.out.println("필터로 커피 우려내는 중");
    }
    
    @Override
    void addCondiments() {
        System.out.println("설탕과 우유 추가 중");
    }
    
    // Hook 메서드 오버라이드
    @Override
    boolean customerWantsCondiments() {
        String answer = getUserInput();
        return answer.toLowerCase().startsWith("y");
    }
    
    private String getUserInput() {
        String answer = null;
        System.out.print("커피에 설탕과 우유를 추가하시겠습니까? (y/n): ");
        
        BufferedReader in = new BufferedReader(
            new InputStreamReader(System.in));
        
        try {
            answer = in.readLine();
        } catch (IOException e) {
            System.err.println("입력 오류");
        }
        
        if (answer == null) {
            return "no";
        }
        return answer;
    }
}

6. Hollywood Principle (할리우드 원칙)

"Don't call us, we'll call you!"

"먼저 연락하지 마세요, 저희가 연락드리겠습니다!"

의미

  • 저수준 컴포넌트고수준 컴포넌트를 직접 호출하지 않음
  • 대신 고수준 컴포넌트가 저수준 컴포넌트를 언제, 어떻게 사용할지 결정
  • 의존성 부패(Dependency Rot) 방지

Template Method에서의 적용

┌─────────────────────────────┐
│  CaffeineBeverage (고수준)   │
│  - prepareRecipe() 제어     │ ← "우리가 호출할게요"
└──────────┬──────────────────┘
           │ 호출
           │
    ┌──────▼─────┐    ┌──────▼─────┐
    │   Coffee   │    │    Tea     │
    │  (저수준)   │    │  (저수준)   │
    └────────────┘    └────────────┘
    "호출 기다림"      "호출 기다림"

Coffee와 Tea는:

  • prepareRecipe()를 직접 호출하지 않음
  • brew()와 addCondiments()만 구현
  • CaffeineBeverage가 언제 호출할지 결정

7. Java API에서의 Template Method 예시

JFrame의 paint()

public class MyFrame extends JFrame {
    @Override
    public void paint(Graphics g) {
        // JFrame이 호출 → Hook처럼 동작
        super.paint(g);
        g.drawString("Template Method!", 50, 50);
    }
}

Applet의 Lifecycle Hooks

public class MyApplet extends Applet {
    @Override
    public void init() {
        // Applet이 초기화 시점에 호출
    }
    
    @Override
    public void start() {
        // Applet이 시작 시점에 호출
    }
    
    @Override
    public void stop() {
        // Applet이 중지 시점에 호출
    }
    
    @Override
    public void destroy() {
        // Applet이 종료 시점에 호출
    }
}

8. Template Method VS Strategy Pattern

비교 항목Template MethodStrategy
메커니즘상속 (Inheritance)위임 (Delegation)
변경 범위알고리즘의 일부 단계만 변경전체 알고리즘 교체 가능
유연성컴파일 타임에 결정 (정적)런타임에 변경 가능 (동적)
결합도부모-자식 강한 결합느슨한 결합
사용 시기알고리즘 골격 공유, 일부만 다를 때알고리즘 전체를 교체해야 할 때

Template Method 예시

abstract class Game {
    final void play() {  // 알고리즘 골격 고정
        initialize();
        startPlay();     // 다름
        endPlay();       // 다름
    }
    
    void initialize() {
        System.out.println("게임 초기화");
    }
    
    abstract void startPlay();
    abstract void endPlay();
}

Strategy 예시

class Game {
    private GameStrategy strategy;  // 알고리즘 전체를 교체
    
    void setStrategy(GameStrategy strategy) {
        this.strategy = strategy;
    }
    
    void play() {
        strategy.execute();  // 전체 알고리즘 위임
    }
}

9. 핵심 정리

Template Method Pattern 구성

요소역할특징
AbstractClass알고리즘 골격 정의Template Method를 final로 선언
Template Method알고리즘 단계 순서 정의서브클래스가 변경 불가
Concrete Methods공통 구현 제공모든 서브클래스에서 재사용
Abstract Methods서브클래스 구현 강제각 서브클래스마다 다른 구현
Hook Methods선택적 확장 지점기본 구현 제공, 오버라이드 가능

언제 사용하는가?

  • 알고리즘의 골격은 동일하지만 일부 단계만 다를 때
  • 중복 코드를 부모 클래스로 모으고 싶을 때
  • 서브클래스의 공통 동작을 제어하고 싶을 때
  • 알고리즘 구조를 변경하지 못하도록 보호하고 싶을 때

장점

  • 코드 재사용: 공통 코드를 부모 클래스에 한 번만 작성
  • 제어 역전: 프레임워크가 서브클래스 메서드를 호출 (Hollywood Principle)
  • 유지보수 용이: 알고리즘 변경 시 한 곳만 수정
  • 일관성 유지: 알고리즘 구조가 모든 서브클래스에서 동일

단점

  • 상속 기반: 클래스 계층이 복잡해질 수 있음
  • 유연성 제한: 런타임에 알고리즘 변경 불가
  • 리스코프 치환 원칙 위반 가능: 서브클래스가 부모의 계약 위반 위험

10. 핵심 원칙

  • 알고리즘 캡슐화: 알고리즘을 템플릿으로 캡슐화
  • 상속을 통한 재사용: 공통 코드를 부모 클래스에서 재사용
  • 할리우드 원칙: 저수준이 아닌 고수준이 제어
  • OCP 준수: 새로운 서브클래스 추가 시 기존 코드 수정 불필요
profile
다들 응원합니다.

0개의 댓글