전략·템플릿 콜백
변하는 실행 조각을 전략과 람다 콜백으로 전달하고 싱글톤 템플릿의 스레드 안전성·예외·반환 타입을 설계합니다.
템플릿 메서드가 “상위 클래스가 하위 클래스를 호출”한다면 전략은 “컨텍스트가 협력 객체를 호출”합니다.
알고리즘 흐름과 변하는 동작을 객체 조합으로 연결해 단일 상속 제약을 피합니다.
변형이 메서드 하나면 콜백 람다로 더 작게 표현할 수 있으며 JdbcTemplate과 TransactionTemplate도 같은 사고방식을 사용합니다.
컨텍스트·전략 역할
컨텍스트는 트레이스 시작·종료·실패 기록을 소유합니다.
전략은 게시글 조회나 등록만 구현합니다.
생성자에 전략을 고정하면 재사용 가능한 객체 조합이고, 실행 파라미터로 받으면 호출마다 다른 콜백을 쓸 수 있습니다.
package board.strategy;
import java.util.Objects;
public final class TraceContext {
private final TraceEvents events;
public TraceContext(TraceEvents events) {
this.events = Objects.requireNonNull(events);
}
public <T> T execute(String operation, Strategy<T> strategy) {
long start = System.nanoTime();
events.started(operation);
try {
T value = strategy.call();
events.finished(operation, System.nanoTime() - start);
return value;
} catch (RuntimeException | Error failure) {
events.failed(operation, failure, System.nanoTime() - start);
throw failure;
}
}
@FunctionalInterface
public interface Strategy<T> {
T call();
}
public interface TraceEvents {
void started(String operation);
void finished(String operation, long nanos);
void failed(String operation, Throwable failure, long nanos);
}
}컨텍스트에 현재 작업을 필드로 저장하지 않아 싱글톤으로 공유해도 호출별 로컬 변수가 분리됩니다.
이벤트 구현도 스레드-안전해야 하며 요청 상태는 ThreadLocal 책임 주체에서 읽거나 인자로 받습니다.
람다 콜백
익명 클래스보다 람다가 짧고 대상 코드가 리포지토리 메서드 가까이에 보입니다.
캡처하는 변수는 사실상 final이어야 하며 변경 가능한 컬렉션을 공유하면 별도 동시성 문제가 생깁니다.
package board.strategy;
public final class CallbackDemo {
public static void main(String[] args) {
var context = new TraceContext(new ConsoleEvents());
long authorId = 41L;
String result = context.execute(
"weekly summary",
() -> "member=" + authorId + " postCount=12");
System.out.println(result);
}
static final class ConsoleEvents implements TraceContext.TraceEvents {
@Override
public void started(String operation) {
System.out.println("start=" + operation);
}
@Override
public void finished(String operation, long nanos) {
System.out.println("outcome=success");
}
@Override
public void failed(String operation, Throwable failure, long nanos) {
System.out.println("outcome=" + failure.getClass().getSimpleName());
}
}
}start=weekly summary
outcome=success
member=41 postCount=12콜백이 너무 길어지면 이름을 붙인 private 메서드나 별도 전략 클래스로 추출합니다.
람다 안에 트랜잭션, 재시도, 검증을 모두 넣으면 흐름이 다시 숨겨집니다.
콜백의 검사 예외
JDK Supplier<T>는 검사 예외를 선언할 수 없습니다.
원시 JDBC 콜백처럼 검사 타입이 실제 확장 계약이면 자체 인터페이스에 throws E를 둘 수 있습니다.
그러나 호출자 전체에 일반 예외 파라미터가 퍼지는 비용을 고려합니다.
Spring 템플릿은 저수준 검사 예외를 콜백 안에서 받고 공통 계층으로 번역합니다.
애플리케이션 트레이스 컨텍스트가 기술 예외 번역까지 맡으면 관심사가 섞이므로 대상 어댑터가 먼저 런타임 규칙을 제공합니다.
콜백에서 예외를 감싸기할 때 원인을 보존하고, 컨텍스트는 원본 인스턴스를 그대로 재전파하는 것이 좋습니다.
트레이싱 계층이 예외 분류 체계를 바꾸면 롤백 규칙도 예상과 달라질 수 있습니다.
TransactionTemplate 콜백
프록시 내부 호출을 피하거나 작은 블록만 트랜잭션으로 묶어야 할 때 TransactionTemplate.execute()가 유용합니다.
콜백은 TransactionStatus를 받아 롤백 전용 상태를 표시할 수 있습니다.
null 반환 허용 규칙과 예외 전파를 API 문서에서 확인합니다.
프로그래밍 방식은 전파와 타임아웃이 구성 객체에 보이지만 애노테이션보다 반복 코드가 늘어납니다.
동적으로 트랜잭션 옵션을 바꾸는 코드는 검토가 어려우므로 미리 구성한 템플릿 빈을 목적별로 둡니다.
리액티브 TransactionalOperator는 발행기 콜백과 Reactor 컨텍스트를 사용합니다.
명령형 ThreadLocal 트랜잭션 모델과 같은 것으로 취급하지 않습니다.
싱글톤 템플릿 상태
작업 이름, 시작 시간, 결과를 컨텍스트 필드에 두면 전략을 적용해도 ch12-1의 경쟁이 돌아옵니다.
execute()의 스택 지역 변수와 불변 상태에 보관합니다.
공유 수집 대상이 버퍼를 가진다면 동시성 컬렉션이나 추가 전용 백엔드를 사용합니다.
콜백을 나중에 실행하도록 저장하면 캡처한 엔티티와 요청 컨텍스트의 수명이 달라집니다.
즉시 실행 템플릿인지 지연된 명령인지 인터페이스 이름에서 구분합니다.
지연된 작업에는 불변 입력 스냅샷과 명시적인 실행자를 사용합니다.
패턴 선택 기준
| 조건 | 템플릿 Method | 템플릿 Callback |
|---|---|---|
| 단계별 훅 다수 | 읽기 쉬움 | 콜백 타입 증가 |
| 한 작업 변형 | 하위 클래스가 무거움 | 람다 적합 |
| 런타임 조합 | 제한적 | 자유로움 |
| 상속 계층 | 필요 | 불필요 |
| 상태 소유 | 부모 인스턴스 주의 | 컨텍스트 로컬 권장 |
패턴 이름을 적용하는 것이 목표가 아니라 복사된 제어 흐름의 책임 주체를 한 곳으로 만드는 것이 목표입니다.
연습 문제
TransactionTemplate와 트레이스 콜백을 중첩해 게시글 등록을 실행하세요.
어떤 래퍼가 바깥에 있어야 커밋 실패까지 트레이스에 포함되는지 결정하고, 런타임 예외 동일성과 롤백 행을 검증하세요.
해설 보기
트레이스 컨텍스트가 트랜잭션 템플릿 바깥을 감싸면 대상 실행뿐 아니라 커밋 단계 예외도 관찰할 수 있습니다.
package board.strategy;
import org.springframework.transaction.support.TransactionTemplate;
public final class TracedTransaction {
private final TraceContext trace;
private final TransactionTemplate transactions;
public TracedTransaction(TraceContext trace, TransactionTemplate transactions) {
this.trace = trace;
this.transactions = transactions;
}
public <T> T execute(String name, TraceContext.Strategy<T> work) {
return trace.execute(name, () -> transactions.execute(status -> work.call()));
}
}반환값이 null일 수 없는 사용 사례라면 콜백 결과를 검사해 규칙 실패로 드러냅니다.