← Posts

GoF · Behavioral · Template Method

Template Method

여러 흐름이 큰 틀은 같은데 한 단계만 다를 때. 뼈대를 상위가 고정하고 달라지는 구멍만 하위가 채우는 행위 패턴.

목차
  1. 흐름은 같은데 일부만 다르다
  2. 템플릿 메서드 = 뼈대는 고정, 일부만 채운다
  3. 상위가 흐름을 정한다
  4. 추상 메서드로 구멍을 낸다
  5. 하위가 구멍을 채운다
  6. 훅 메서드로 선택 지점을 연다
  7. 상위가 하위를 부른다 - 할리우드 원칙
  8. 실무: 이름에 Template이 붙는 것들
  9. 정리

흐름은 같은데 일부만 다르다

파일을 읽어 데이터를 처리하는 코드가 여럿 있다고 하자. CSV를 읽는 것, JSON을 읽는 것 - 큰 틀은 똑같다. 열고, 파싱하고, 결과를 쓴다.

java
// CSV 처리
void processCsv(String path) {
    open(path);
    Data d = parseCsv();      // 여기만 다르다
    write(d);
    close();
}

// JSON 처리
void processJson(String path) {
    open(path);
    Data d = parseJson();     // 여기만 다르다
    write(d);
    close();
}

열기·쓰기·닫기는 글자 하나 안 다르다. 오직 파싱 방식만 다르다. 그런데 흐름 전체가 두 번 복붙됐다. 처리기가 하나 더 늘면(XML, 로그 등) 그 흐름이 또 통째로 복사된다. 열고 닫는 순서를 바꾸거나 실패 처리를 하나 끼우려 하면, 복사본을 전부 찾아 똑같이 고쳐야 한다.

템플릿 메서드 = 뼈대는 고정, 일부만 채운다

템플릿 메서드 패턴이 이걸 푼다.

알고리즘의 **뼈대(흐름)**는 상위 클래스가 고정하고, 흐름 안에서 달라지는 단계만 하위 클래스가 채우게 한다.

열고-파싱하고-쓰고-닫는 순서는 한 곳에 못 박아 둔다. 그 순서 안에 “파싱” 자리만 빈칸으로 남겨, CSV용·JSON용이 그 빈칸만 서로 다르게 메운다. 흐름은 한 번만 쓰고, 다른 것만 갈아 끼운다.

상위가 흐름을 정한다

상위 추상 클래스에 흐름을 담은 메서드를 하나 둔다. 이게 템플릿 메서드다.

java
public abstract class DataProcessor {

    // 흐름(뼈대) - 여기서 순서가 고정된다
    public final void process(String path) {
        open(path);
        Data d = parse();      // 달라지는 단계
        write(d);
        close();
    }

    private void open(String path) { /* 공통 */ }
    private void write(Data d)     { /* 공통 */ }
    private void close()           { /* 공통 */ }

    protected abstract Data parse();   // 하위가 채울 구멍
}

process()에 **final**이 붙은 게 핵심이다. 흐름 자체는 하위가 못 건드린다. 열고-파싱하고-쓰고-닫는 순서는 상위가 정했고, 하위는 그 순서를 바꿀 수 없다. 바꿀 수 있는 건 순서가 아니라 각 단계의 내용뿐이다.

diagramdiagram

추상 메서드로 구멍을 낸다

흐름 안에서 달라지는 단계는 추상 메서드로 남긴다. 위 코드의 parse()가 그것이다.

상위 클래스는 parse()가 무엇을 어떻게 하는지 모른다. 다만 “흐름의 이 지점에서 parse()를 부른다”는 것만 안다. 실제 파싱은 하위가 채울 몫으로 비워 둔다. 흐름에 뚫린 구멍인 셈이다.

이 구멍이 있어서 상위는 세부를 몰라도 흐름을 완성할 수 있다. 무엇이 들어올지 모른 채로 “여기서 파싱한다”는 자리만 잡아 둔다.

하위가 구멍을 채운다

하위 클래스는 추상 메서드만 구현한다. 흐름은 건드리지 않는다.

java
public class CsvProcessor extends DataProcessor {
    @Override
    protected Data parse() {
        // CSV 파싱
        return new Data(/* ... */);
    }
}

public class JsonProcessor extends DataProcessor {
    @Override
    protected Data parse() {
        // JSON 파싱
        return new Data(/* ... */);
    }
}

부르는 쪽은 이렇다.

java
DataProcessor processor = new CsvProcessor();
processor.process("data.csv");

process()는 상위에 한 번만 쓰여 있다. CsvProcessorJsonProcessor도 흐름을 복사하지 않았다. 각자 parse()라는 구멍만 다르게 메웠을 뿐이다. 처리기를 하나 더 추가해도 새 parse() 하나면 끝이고, 흐름을 고칠 일이 생기면 상위 process() 한 곳만 고치면 모든 처리기에 반영된다.

훅 메서드로 선택 지점을 연다

가끔은 “필수로 채워야 하는 구멍”이 아니라 “채우면 좋고 안 채워도 되는 자리”가 필요하다. 이걸 훅(hook) 메서드로 둔다.

훅은 추상 메서드와 달리 상위에 **기본 구현(보통 비어 있음)**을 두어, 하위가 필요할 때만 재정의한다.

java
public abstract class DataProcessor {

    public final void process(String path) {
        open(path);
        Data d = parse();
        if (shouldValidate()) {   // 훅으로 흐름 일부를 켜고 끈다
            validate(d);
        }
        write(d);
        close();
    }

    protected abstract Data parse();

    // 훅 - 기본은 false, 필요한 하위만 켠다
    protected boolean shouldValidate() { return false; }

    private void validate(Data d) { /* ... */ }
}

parse()는 반드시 구현해야 하지만 shouldValidate()는 그대로 둬도 된다. 검증이 필요한 처리기만 이 훅을 true로 재정의한다. 흐름의 특정 지점을 하위가 선택적으로 켜고 끄게 하는 것이 훅이다.

상위가 하위를 부른다 - 할리우드 원칙

여기서 흐름의 방향을 잘 봐야 한다. 보통은 내가 짠 코드가 라이브러리를 부른다. 그런데 템플릿 메서드에서는 반대다. 상위 클래스의 process()가 하위의 parse()를 부른다.

이걸 할리우드 원칙이라 한다. “먼저 연락하지 마세요, 필요하면 저희가 부릅니다(Don’t call us, we’ll call you).” 하위 클래스는 자기 parse()가 언제 불릴지 정하지 않는다. 그저 구현만 해 두고, 부르는 시점은 상위에 맡긴다.

제어의 주도권이 상위에 있다. 하위는 “무엇을”만 채우고, “언제”는 상위가 쥔다. 흐름을 상위가 소유했기 때문에 하위가 아무리 늘어도 흐름의 일관성이 흔들리지 않는다.

실무: 이름에 Template이 붙는 것들

Spring에는 이름에 Template이 붙은 클래스가 많다. JdbcTemplate, RestTemplate, TransactionTemplate 등이다. 이 발상이 바로 템플릿 메서드다.

JdbcTemplate을 예로 들면, “커넥션 열기 → 쿼리 실행 → 결과 매핑 → 커넥션 닫기 → 예외 변환”이라는 흐름은 프레임워크가 고정한다. 우리는 그 흐름 안에서 달라지는 “결과를 어떻게 객체로 매핑할지”만 RowMapper로 채운다. 커넥션을 여닫고 예외를 처리하는 반복 코드를 우리가 다시 쓰지 않는 이유가 이것이다.

HttpServlet도 같다. service()가 요청 방식을 보고 doGet()·doPost()를 골라 부른다. 흐름은 service()가 쥐고, 우리는 doGet()이라는 구멍만 채운다. 우리가 service()를 부르는 게 아니라, 컨테이너가 우리 doGet()을 불러 준다 - 할리우드 원칙 그대로다.

정리

템플릿 메서드란흐름의 뼈대는 상위가 고정하고, 달라지는 단계만 하위가 채운다
문제큰 틀이 같은 흐름이 한 단계 때문에 통째로 복붙된다
고침상위에 흐름을 담은 final 메서드, 달라지는 곳은 추상 메서드로
기본 구현을 둔 선택적 자리 - 필요한 하위만 재정의
할리우드 원칙상위가 하위를 부른다 - 하위는 무엇만, 언제는 상위가
실무JdbcTemplate*Template, HttpServletservicedoGet

흐름은 한 번만 못 박아 두고 달라지는 구멍만 하위가 메우게 하는 것 - 그게 템플릿 메서드다.

관련 글