템플릿 메서드
템플릿 메서드로 공통 제어 흐름을 고정하고 상속 결합·예외 재전파·확장 지점을 판단합니다.
컨트롤러와 리포지토리마다 begin, try, end, catch를 복사하면 핵심 로직보다 추적 코드가 더 크게 보입니다.
템플릿 Method 패턴은 실행 순서를 상위 클래스가 소유하고 하위 클래스가 달라지는 작업만 구현하게 합니다.
상속은 흐름을 강하게 고정하는 대신 부모 내부 지식과 생명주기에 결합된다는 비용이 있습니다.
공통 흐름과 변형
게시글 조회와 등록 모두 트레이스 시작, 대상 호출, 정상 종료, 예외 종료가 같습니다.
SQL과 반환 타입만 다릅니다.
공통 블록이 단순히 앞뒤 두 줄인지, 예외 매핑과 리소스 정리까지 포함하는지 써 보면 추상화 경계를 찾기 쉽습니다.
템플릿은 final execute()로 순서를 고정하고 protected abstract call()을 확장 지점으로 둡니다.
하위 클래스가 시작이나 정리를 재정의하게 열어 두면 불변 흐름이 다시 깨집니다.
package board.template;
public abstract class TraceTemplate<T> {
private final TraceSink sink;
protected TraceTemplate(TraceSink sink) {
this.sink = sink;
}
public final T execute(String message) {
long started = System.nanoTime();
sink.begin(message);
try {
T result = call();
sink.success(message, System.nanoTime() - started);
return result;
} catch (RuntimeException | Error failure) {
sink.failure(message, failure, System.nanoTime() - started);
throw failure;
}
}
protected abstract T call();
public interface TraceSink {
void begin(String message);
void success(String message, long elapsedNanos);
void failure(String message, Throwable failure, long elapsedNanos);
}
}일반 반환 타입을 사용해 쿼리와 명령 결과를 그대로 돌려줍니다.
검사 예외까지 지원하려면 별도 함수형 규칙과 타입 파라미터가 필요하지만 모든 호출자에 검사 타입을 확산시키는지 다시 검토합니다.
익명 하위 클래스
다음 실행은 한 번의 게시글 조회를 익명 하위 클래스로 감쌉니다.
대상 예외는 로그 뒤 같은 인스턴스로 재전파됩니다.
package board.template;
public final class TemplateMethodDemo {
public static void main(String[] args) {
var sink = new ConsoleSink();
var operation = new TraceTemplate<String>(sink) {
@Override
protected String call() {
return "post-41";
}
};
System.out.println("result=" + operation.execute("find post"));
}
static final class ConsoleSink implements TraceTemplate.TraceSink {
@Override
public void begin(String message) {
System.out.println("BEGIN " + message);
}
@Override
public void success(String message, long elapsedNanos) {
System.out.println("END " + message + " success");
}
@Override
public void failure(
String message,
Throwable failure,
long elapsedNanos
) {
System.out.println("END " + message + " failure="
+ failure.getClass().getSimpleName());
}
}
}BEGIN find post
END find post success
result=post-41익명 클래스가 여러 메서드에 반복되기 시작하면 람다를 받는 콜백 구조가 더 간결합니다.
템플릿 메서드는 알고리즘 단계마다 재정의가 필요하거나 하위 클래스 타입 자체가 의미 있을 때 적합합니다.
예외·자원 정리 불변식
성공 로그를 finally에 두면 실패도 성공으로 기록됩니다.
예외 처리에서 실패를 기록하고 원래 예외를 다시 던집니다.
정리가 필요하면 별도 finally를 두되 정리 예외가 원래 업무 실패를 덮지 않게 억제된 관계를 보존합니다.
트레이서가 예외 메시지 전체를 출력하면 인증 정보가나 SQL 파라미터가 유출될 수 있습니다.
수집 대상은 실패 클래스와 안정적인 코드를 받고 스택 트레이스는 요청 경계 정책이 결정합니다.
템플릿의 책임을 로깅 백엔드 설정까지 넓히지 않습니다.
작업이 null을 반환해도 성공인지, 선택적 결과를 요구할지 규칙에 적습니다.
템플릿이 null을 임의의 실패로 바꾸면 대상 의미를 침범합니다.
상속 변경 전파
하위 클래스는 부모의 호출 순서와 protected API를 알아야 합니다.
생성자 의존성이 늘거나 훅 순서가 바뀌면 모든 하위 클래스가 영향을 받습니다.
Java는 단일 상속이라 이미 다른 기반 클래스가 있는 컴포넌트에는 적용하기 어렵습니다.
상속을 실제 도메인 하위 타입 관계로 오해하지 않습니다.
PostRepository extends TraceTemplate는 리포지토리가 트레이스 템플릿의 한 종류라는 모델이 아니라 코드 재사용 관계입니다.
객체 조합 기반의 전략과 콜백은 이 결합을 줄입니다.
final 템플릿은 순서를 보호하지만 프록시가 하위 클래스를 만들어야 하는 환경과 충돌할 수 있습니다.
이 템플릿 객체 자체를 Spring AOP 대상으로 삼을지, 내부 도우미로만 쓸지 구분합니다.
호출 순서·예외 검증
기록 수집 대상에 이벤트를 모아 BEGIN → END 순서를 확인하고 대상 실패와 호출자가 받은 객체가 같은지 봅니다.
지속 시간 숫자는 가짜 시간 제공자를 주입해 결정적으로 만듭니다.
콘솔 문자열 스냅샷만 비교하면 핵심 순서보다 포매팅 변화에 취약합니다.
템플릿 메서드 선택 기준
공통 순서가 안정적이고 변형 지점이 소수이며 하위 클래스가 자연스러운 이름을 가질 때 템플릿 메서드가 읽기 좋습니다.
런타임에 작업을 바꾸거나 조합하고 싶고 변형이 람다 하나라면 콜백이 낫습니다.
공통 코드가 두 줄뿐이라면 추상화가 오히려 탐색 비용을 만들 수 있습니다.
연습 문제
재시도를 템플릿 단계로 추가하지 말고 한 번의 시도만 추적하도록 유지하세요.
호출자가 재시도할 때 각 시도와 전체 작업을 구별할 이벤트 모델을 설계하고 실패 동일성이 보존되는 테스트를 작성하세요.
해설 보기
시도 템플릿과 재시도 조정자를 분리하면 트레이싱이 업무 재시도 정책을 소유하지 않습니다.
package board.template;
public final class RetryCoordinator {
public <T> T run(int maximumAttempts, Attempt<T> attempt) {
RuntimeException last = null;
for (int number = 1; number <= maximumAttempts; number++) {
try {
return attempt.run(number);
} catch (RuntimeException failure) {
last = failure;
}
}
throw last;
}
@FunctionalInterface
public interface Attempt<T> {
T run(int number);
}
}실제 재시도는 멱등성, 일시적 분류, 기한, 백오프를 추가해야 합니다.
템플릿은 각 시도의 관찰만 담당합니다.