[3] 디자인패턴: 템플릿 패턴 (Template Method/Callback)

2025. 12. 29. 00:51·Java/Design Pattern

1. 들어가며

 백엔드 개발의 숙명 중 하나는 '반복되는 코드(Boilerplate Code)'와의 싸움이다. 로그 기록, 트랜잭션 시작과 종료, 자원 해제(try-catch-finally) 등 비즈니스 로직과는 직접적인 상관이 없지만 반드시 거쳐야 하는 일련의 흐름들이 존재한다. 이러한 알고리즘의 뼈대를 고정하고 변하는 부분만 선택적으로 구현하게 해주는 것이 바로 템플릿 패턴이다.

 

 소프트웨어 설계에서 가장 중요한 원칙 중 하나는 '변하는 것'과 '변하지 않는 것'을 엄격히 분리하는 것이다. 템플릿 패턴은 전체적인 작업 순서(변하지 않는 것)는 부모 클래스나 템플릿에 정의해두고, 구체적인 세부 로직(변하는 것)만 자식 클래스나 외부 콜백에서 구현하도록 설계한다.

 

 스프링 프레임워크에서는 이 패턴을 통해 개발자가 핵심 비즈니스 로직에만 집중할 수 있도록 돕는다. 대표적인 예가 JdbcTemplate이나 RestTemplate인 것이다.


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

 고전적인 GoF의 템플릿 메서드 패턴은 상속을 기반으로 한다. 부모 클래스에서 알고리즘의 골격을 정의하고, 일부 단계를 자식 클래스에서 오버라이딩하도록 만든다.

2.1. 구조와 특징

  • Abstract Class: 알고리즘의 뼈대를 정의하는 templateMethod()를 가진다.
  • Hook Method: 자식 클래스에서 선택적으로 오버라이딩할 수 있거나, 반드시 구현해야 하는 추상 메서드이다.

2.2. 코드 예시: 실행 시간 측정 로그

모든 서비스 로직의 실행 시간을 측정해야 한다고 가정해보자.

// 부모 클래스: 템플릿 정의
public abstract class AbstractTemplate {

    public void execute() {
        long startTime = System.currentTimeMillis();
        
        // 변하는 로직 수행 (Hook Method 호출)
        call();
        
        long endTime = System.currentTimeMillis();
        System.out.println("실행 시간: " + (endTime - startTime) + "ms");
    }

    protected abstract void call(); // 자식 클래스에서 구현할 부분
}
// 자식 클래스 1: 비즈니스 로직 A
public class LogicATemplate extends AbstractTemplate {
    @Override
    protected void call() {
        System.out.println("비즈니스 로직 A 실행");
    }
}

2.3. 한계점

 템플릿 메서드 패턴은 상속을 사용하기 때문에 자바의 단일 상속 제한에 걸릴 수 있으며, 부모 클래스와 자식 클래스가 컴파일 시점에 강하게 결합된다는 단점이 있다. 자식 클래스는 부모 클래스의 기능을 전혀 쓰지 않더라도 상속으로 인해 부모의 모든 것을 짊어져야 하는 것이다.


3. 템플릿 콜백 패턴 (Template Callback Pattern ★)

 스프링은 위의 [2. 템플릿 메서드 패턴]의 고질적인 상속 단점을 극복하기 위해 전략 패턴과 익명 내부 클래스(또는 람다)를 결합한 '템플릿 콜백 패턴'을 즐겨 사용한다. 이는 상속이 아닌 합성(Composition)을 이용한 방식이다.

3.1. 전략 패턴과의 관계

 템플릿 콜백 패턴은 전략 패턴의 일종이라고 볼 수 있다. 다만, 전략 객체를 실행 시점에 주입받아 고정된 템플릿 안에서 호출(Callback)한다는 점이 핵심이다.

3.2. 스프링에서의 실전 구현

스프링 내부에서 가장 흔히 볼 수 있는 구조로, Callback 인터페이스를 넘겨받아 로직을 수행한다.

// 콜백 인터페이스
public interface Callback {
    void call();
}
// 템플릿 클래스
@Component
public class TimeLogTemplate {
    public void execute(Callback callback) {
        long startTime = System.currentTimeMillis();
        
        // 넘겨받은 콜백 로직 수행
        callback.call();
        
        long endTime = System.currentTimeMillis();
        System.out.println("실행 시간: " + (endTime - startTime) + "ms");
    }
}
// 서비스 레이어에서의 사용
@Service
@RequiredArgsConstructor
public class OrderService {
    private final TimeLogTemplate template;

    public void order() {
        template.execute(() -> {
            // 여기에만 비즈니스 로직 작성
            System.out.println("주문 로직 실행");
        });
    }
}

4. 실무에서의 응용과 스프링 내부 사례 (1): JdbcTemplate

 과거의 JDBC 연동 코드는 커넥션을 맺고, 문장을 만들고, 결과를 처리한 뒤 자원을 반납하는 복잡한 try-catch-finally 문으로 가득했다. 스프링의 JdbcTemplate은 이 모든 반복을 템플릿에 가두었다.

// 개발자는 쿼리와 결과 매핑(콜백)만 신경 쓰면 된다.
jdbcTemplate.query("SELECT * FROM member", (rs, rowNum) -> {
    return new Member(rs.getLong("id"), rs.getString("name"));
});

5. 실무에서의 응용과 스프링 내부 사례 (2): RedisTemplate

 RedisTemplate 역시 스프링 데이터 레디스(Spring Data Redis)에서 제공하는 대표적인 템플릿 콜백 패턴의 구현체인 것이다. 레디스 조작에 필요한 복잡한 저수준 작업들을 템플릿 내부로 숨겨 개발자가 비즈니스 로직에만 집중할 수 있게 한다.

5.1. RedisTemplate과 템플릿 콜백의 결합

레디스 명령을 실행하기 위해서는 커넥션 획득, 데이터 직렬화, 예외 변환, 커넥션 반환이라는 일련의 고정된 흐름이 필요하다. RedisTemplate은 이러한 반복적인 엔진 역할을 수행하고, 실제 어떤 명령(SET, GET 등)을 내릴지는 콜백을 통해 전달받는 구조인 것이다. 그 핵심에는 execute() 메서드가 존재하며, 이는 RedisCallback 인터페이스를 파라미터로 받는다.

public void customOperation() {
    // 저수준의 RedisCallback을 직접 사용하여 템플릿의 execute() 호출
    redisTemplate.execute((RedisConnection connection) -> {
        // 커넥션 획득 및 해제는 템플릿이 담당하며,
        // 개발자는 콜백 안에서 실제 명령만 수행한다.
        connection.set("my_key".getBytes(), "my_value".getBytes());
        return null;
    });
}

5.2. 추상화의 묘미: 편의성 인터페이스 (opsForValue)

 실무에서는 위와 같이 execute()를 직접 호출하기보다 opsForValue(), opsForList() 등의 편의 메서드를 주로 사용한다. 하지만 이들은 완전히 새로운 기술이 아니라, 템플릿 콜백 로직을 한 번 더 감싸서 제공하는 '포장지'에 불과한 것이다. 예를 들어 redisTemplate.opsForValue().get("key")를 호출하면, 내부적으로는 다음과 같은 흐름이 발생한다.

  1. opsForValue()가 ValueOperations 객체를 반환한다.
  2. get("key") 메서드가 호출되면, 내부적으로 RedisCallback 구현체를 생성한다.
  3. 생성된 콜백이 다시 RedisTemplate.execute()로 넘겨져 실행된다.

 즉, 고수준 API를 사용하더라도 그 밑바닥에는 여전히 템플릿 콜백 패턴이 견고하게 작동하고 있는 셈이다. 이러한 이중 추상화 덕분에 개발자는 콜백 코드를 직접 작성하는 수고조차 덜게 되는 것이다.

5.3. 실무에서 저수준 execute()를 직접 사용하는 경우

 그렇다면 왜 굳이 execute()라는 복잡한 구조를 알아야 하는가? 편의 메서드만으로는 해결하기 어려운 특수한 상황이 존재하기 때문이다.

  • 파이프라이닝(Pipelining): 수백 개의 명령을 한 번에 전송하여 네트워크 오버헤드를 줄여야 할 때.
  • 트랜잭션 세밀한 제어: multi(), exec() 등을 사용하여 원자적 연산을 정교하게 처리해야 할 때.
  • 성능 최적화: 직렬화 과정을 직접 제어하여 바이트 배열을 최적으로 다뤄야 할 때.

 이처럼 일반적인 상황에서는 편의 메서드를 사용하되, 특수한 성능 최적화가 필요한 시점에는 템플릿 콜백의 본체인 execute()를 직접 꺼내 쓰는 것이 시니어 개발자의 역량인 것이다.


6. 장단점 및 주의사항

6.1. 장점

  • 코드 재사용성: 동일한 작업 흐름을 한곳에서 관리하므로 유지보수가 매우 용이하다.
  • 가독성 향상: 비즈니스 로직이 인프라 코드와 분리되어 핵심 의도가 명확히 드러난다.
  • OCP(개방-폐쇄 원칙) 준수: 전체 구조를 변경하지 않고도 새로운 세부 로직을 추가할 수 있다.

6.2. 단점 및 주의점

  • 설계의 복잡도: 콜백이나 상속 구조가 많아지면 코드의 흐름을 한눈에 파악하기 어려울 수 있다.
  • AOP와의 경계: 템플릿 패턴은 코드 수준에서 중복을 제거하지만, 서비스 전체에 적용되는 공통 관심사(로그, 트랜잭션 등)를 처리하기에는 한계가 있다. 모든 메서드에 템플릿 코드를 작성해야 하기 때문이다.

7. 요약

 템플릿 패턴은 "틀(Skeleton)을 만들고 내용물(Logic)만 갈아 끼우는 전략"이다. 상속을 통한 고전적인 '템플릿 메서드 패턴'에서 시작하여, 스프링이 사랑하는 유연한 '템플릿 콜백 패턴'으로 진화하였다. 우리가 무심코 사용하는 JdbcTemplate, RestTemplate, RedisTemplate의 내부에는 모두 이 패턴이 흐르고 있다. 실무에서 자꾸 반복되는 try-catch-finally나 자원 관리 로직이 보인다면, 그것을 템플릿이라는 그릇에 담아낼 때가 되었다는 신호인 것이다.

'Java > Design Pattern' 카테고리의 다른 글

[6] 디자인패턴: 팩토리 패턴 (Factory)  (0) 2025.12.29
[5] 디자인패턴: 데코레이터 패턴 (Decorator)  (0) 2025.12.29
[4] 디자인패턴: 프록시 패턴 (Proxy)  (0) 2025.12.29
[2] 디자인패턴: 전략 패턴 (Strategy)  (0) 2025.12.28
[1] 디자인패턴: 왜 스프링 부트 개발자에게 GoF는 필수인가  (0) 2025.12.28
'Java/Design Pattern' 카테고리의 다른 글
  • [5] 디자인패턴: 데코레이터 패턴 (Decorator)
  • [4] 디자인패턴: 프록시 패턴 (Proxy)
  • [2] 디자인패턴: 전략 패턴 (Strategy)
  • [1] 디자인패턴: 왜 스프링 부트 개발자에게 GoF는 필수인가
h6bro
h6bro
백엔드 개발자의 기술 블로그
  • h6bro
    Jun's Tech Blog
    h6bro
  • 전체
    오늘
    어제
    • 분류 전체보기 (250) N
      • Java (18)
        • Core (9)
        • Design Pattern (9)
      • Spring (80)
        • Core (24)
        • MVC (6)
        • DB (10)
        • JPA (26)
        • Monitoring (3)
        • Security (11)
        • WebSocket (0)
      • Database (33)
        • Redis (15)
        • MySQL (18)
      • MSA (25) N
        • MSA 기본 (11)
        • MSA 아키텍처 (14) N
      • Kafka (30)
        • Core (18)
        • Connect (12)
      • ElasticSearch (11)
        • Search (11)
        • Logging (0)
      • Test (4)
        • k6 (4)
      • Docker (9)
      • CI&CD (10)
        • GitHub Actions (6)
        • ArgoCD (4)
      • Kubernetes (18)
        • Core (12)
        • Ops (6)
      • Cloud Engineering (4)
        • AWS Infrastructure (3)
        • AWS EKS (1)
        • Terraform (0)
      • Project (8)
        • LinkFolio (1)
        • Secondhand Market (7)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • Cloud Engineering 포스팅 정리
  • 인기 글

  • 태그

    ㅈ
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
h6bro
[3] 디자인패턴: 템플릿 패턴 (Template Method/Callback)
상단으로

티스토리툴바