템플릿 메서드
템플릿 메서드로 공통 제어 흐름을 고정하고 상속 결합·예외 재전파·확장 지점을 판단합니다.
컨트롤러와 리포지토리마다 begin, try, end, catch를 복사하면 핵심 로직보다 추적 코드가 더 크게 보입니다. 템플릿 메서드 패턴(template method pattern)은 실행 순서를 상위 클래스가 정하고, 하위 클래스는 달라지는 작업만 구현하게 합니다.
상속(inheritance)은 흐름을 단단히 고정해 주는 대신, 하위 클래스를 부모의 내부 구조와 생명 주기에 묶는 비용이 있습니다. 이 글은 하나의 트레이스 템플릿을 만들어 보고, 그 템플릿이 무엇을 보장하고 무엇은 보장하지 않는지를 예외 경로까지 따라가며 확인합니다.
템플릿 메서드
begin 뒤 try의 업무 반환·success 또는 failure 재전파로 제어가 이동하며 sink 무실패 가정을 유지해야 한다.
TEMPLATE METHOD · FIXED FLOW + OPERATION HOOK
final execute가 순서를 소유하고 hook만 바꾼다
상위 템플릿이 시작·대상 호출·정상/실패 기록을 고정합니다. 하위 클래스는 protected call()에 한 작업의 변형만 제공합니다.
- final execute → begin → call
상위 클래스가 순서를 고정하고 하위 hook은 한 업무 작업만 수행합니다.
- call의 반환 / RuntimeException·Error
정상은 success 뒤 원본 결과, 실패는 failure 뒤 같은 예외를 다시 던집니다. sink가 실패하지 않는 예제 조건입니다.
예제가 보장하는 범위
원문에는 finally와 suppressed 처리가 없습니다. sink.failure가 던지면 업무 실패를 덮을 수 있습니다. 관찰·정리 실패 격리는 추가 구현해야 합니다.
상속 비용과 선택
단계별 hook이 늘면 부모 변경이 하위 타입으로 번집니다. 작업 하나의 변형이나 런타임 조합에는 callback이 가볍습니다. 이 final 템플릿을 클래스 프록시 대상으로 삼을지도 구분합니다.
성공 로그는 finally에 두지 않습니다. 재시도 조정자는 별도 책임이며 템플릿은 각 시도를 관찰합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- begin은 try 밖에서 호출한다. try 안의 call이 정상 반환하면 success를 기록한 뒤 원래 결과를 반환한다. RuntimeException 또는 Error면 failure를 기록한 뒤 같은 객체를 throw한다. 그림은 sink의 세 메서드가 실패하지 않는 조건이다. 그 조건을 제거하면 begin 실패는 catch 밖이고 success 실패는 catch로 가며, failure의 새 실패는 원래 실패를 덮을 수 있다. 정상 반환과 실패 종료 사이에 간선은 없다.
- execute에서 begin을 기록한 뒤 하위 call을 실행한다. 정상 결과는 success 뒤 반환한다. RuntimeException 또는 Error는 failure 기록 뒤 같은 객체를 다시 던진다. 이 계약은 sink가 실패하지 않을 때 예제에서 유지된다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 템플릿 메서드 |
| 8 | begin은 try 밖에서 호출한다. try 안의 call이 정상 반환하면 success를 기록한 뒤 원래 결과를 반환한다. RuntimeException 또는 Error면 failure를 기록한 뒤 같은 객체를 throw한다. 그림은 sink의 세 메서드가 실패하지 않는 조건이다. 그 조건을 제거하면 begin 실패는 catch 밖이고 success 실패는 catch로 가며, failure의 새 실패는 원래 실패를 덮을 수 있다. 정상 반환과 실패 종료 사이에 간선은 없다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 두 분기의 공통 시작 sink.begin을 try 밖에서 호출한 뒤 call에 들어갑니다. 정상과 업무 실패는 같은 시작을 갖는 대안입니다. | 정상 경로: begin → call 진입 · 실패 경로: begin → call 진입 · 조건: sink 세 메서드 무실패 | begin이 완료되어야 try 안의 업무 call을 실행합니다. |
| call 결과별 기록 call이 반환한 경로는 success를 기록하고 RuntimeException·Error를 던진 경로는 catch에서 failure를 기록합니다. | 정상 경로: 원 반환값 보관 → success · 실패 경로: 원 실패 보관 → failure · 실패 범위: RuntimeException·Error | call의 반환 또는 throw가 각 분기의 기록 메서드를 결정합니다. |
| 각 경로의 독립 종료 정상 경로는 보관한 반환값을 돌려주고 실패 경로는 기록한 것과 같은 실패 객체를 재전파합니다. 두 종료 사이 이동은 없습니다. | 정상 경로: 원 반환값 return · 실패 경로: 동일 실패 객체 throw · sink 조건 제거: begin은 catch 밖, failure의 새 실패는 원 실패를 덮을 수 있음 | 기록기 무실패 조건에서 success 뒤 return 또는 failure 뒤 동일 객체 throw로 각 경로가 끝납니다. |
템플릿 메서드
begin 뒤 try의 업무 반환·success 또는 failure 재전파로 제어가 이동하며 sink 무실패 가정을 유지해야 한다.
TEMPLATE METHOD · FIXED FLOW + OPERATION HOOK
final execute가 순서를 소유하고 hook만 바꾼다
상위 템플릿이 시작·대상 호출·정상/실패 기록을 고정합니다. 하위 클래스는 protected call()에 한 작업의 변형만 제공합니다.
- final execute → begin → call
상위 클래스가 순서를 고정하고 하위 hook은 한 업무 작업만 수행합니다.
- call의 반환 / RuntimeException·Error
정상은 success 뒤 원본 결과, 실패는 failure 뒤 같은 예외를 다시 던집니다. sink가 실패하지 않는 예제 조건입니다.
예제가 보장하는 범위
원문에는 finally와 suppressed 처리가 없습니다. sink.failure가 던지면 업무 실패를 덮을 수 있습니다. 관찰·정리 실패 격리는 추가 구현해야 합니다.
상속 비용과 선택
단계별 hook이 늘면 부모 변경이 하위 타입으로 번집니다. 작업 하나의 변형이나 런타임 조합에는 callback이 가볍습니다. 이 final 템플릿을 클래스 프록시 대상으로 삼을지도 구분합니다.
성공 로그는 finally에 두지 않습니다. 재시도 조정자는 별도 책임이며 템플릿은 각 시도를 관찰합니다.
이 예의 조건
- 두 박스는 같은 실행의 진행 정보와 자료·결과를 관찰하는 고정 역할입니다. 박스의 좌우·위아래 위치를 별도 호출 순서로 해석하지 않습니다.
- 조건별 종료는 대안입니다. 실패 뒤 정상 후속 단계를 실행하거나 독립 비교 사례를 같은 요청으로 연결하지 않습니다.
- begin은 try 밖에서 호출한다. try 안의 call이 정상 반환하면 success를 기록한 뒤 원래 결과를 반환한다. RuntimeException 또는 Error면 failure를 기록한 뒤 같은 객체를 throw한다. 그림은 sink의 세 메서드가 실패하지 않는 조건이다. 그 조건을 제거하면 begin 실패는 catch 밖이고 success 실패는 catch로 가며, failure의 새 실패는 원래 실패를 덮을 수 있다. 정상 반환과 실패 종료 사이에 간선은 없다.
- execute에서 begin을 기록한 뒤 하위 call을 실행한다. 정상 결과는 success 뒤 반환한다. RuntimeException 또는 Error는 failure 기록 뒤 같은 객체를 다시 던진다. 이 계약은 sink가 실패하지 않을 때 예제에서 유지된다.
관계도의 보충 설명
| 순번 | 원문 표시 |
|---|---|
| 1 | 템플릿 메서드 |
| 8 | begin은 try 밖에서 호출한다. try 안의 call이 정상 반환하면 success를 기록한 뒤 원래 결과를 반환한다. RuntimeException 또는 Error면 failure를 기록한 뒤 같은 객체를 throw한다. 그림은 sink의 세 메서드가 실패하지 않는 조건이다. 그 조건을 제거하면 begin 실패는 catch 밖이고 success 실패는 catch로 가며, failure의 새 실패는 원래 실패를 덮을 수 있다. 정상 반환과 실패 종료 사이에 간선은 없다. |
| 장면 | 관찰 값 | 전환 이유 |
|---|---|---|
| 두 분기의 공통 시작 sink.begin을 try 밖에서 호출한 뒤 call에 들어갑니다. 정상과 업무 실패는 같은 시작을 갖는 대안입니다. | 정상 경로: begin → call 진입 · 실패 경로: begin → call 진입 · 조건: sink 세 메서드 무실패 | begin이 완료되어야 try 안의 업무 call을 실행합니다. |
| call 결과별 기록 call이 반환한 경로는 success를 기록하고 RuntimeException·Error를 던진 경로는 catch에서 failure를 기록합니다. | 정상 경로: 원 반환값 보관 → success · 실패 경로: 원 실패 보관 → failure · 실패 범위: RuntimeException·Error | call의 반환 또는 throw가 각 분기의 기록 메서드를 결정합니다. |
| 각 경로의 독립 종료 정상 경로는 보관한 반환값을 돌려주고 실패 경로는 기록한 것과 같은 실패 객체를 재전파합니다. 두 종료 사이 이동은 없습니다. | 정상 경로: 원 반환값 return · 실패 경로: 동일 실패 객체 throw · sink 조건 제거: begin은 catch 밖, failure의 새 실패는 원 실패를 덮을 수 있음 | 기록기 무실패 조건에서 success 뒤 return 또는 failure 뒤 동일 객체 throw로 각 경로가 끝납니다. |
그림은 final execute가 try 밖에서 sink.begin을 호출한 뒤, try 안에서 하위 클래스의 call()을 실행하는 흐름입니다. call()이 정상 반환하면 sink.success를 기록하고 원래 결과를 돌려주며, RuntimeException이나 Error를 던지면 sink.failure를 기록한 뒤 같은 객체를 다시 던집니다. 두 경로는 같은 시작을 공유하는 대안이고, 정상 반환과 실패 종료 사이를 오가는 간선은 없습니다. 그림은 기록기(sink)의 세 메서드가 실패하지 않는다는 조건에서 그렸으며, 이 조건을 빼면 어떤 일이 생기는지는 아래 예외 절에서 다룹니다.
공통 흐름과 변형
게시글 조회와 등록은 모두 트레이스 시작, 대상 호출, 정상 종료, 예외 종료의 순서가 같고 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);
}
}제네릭 반환 타입 T 덕분에 조회 결과와 명령 결과를 그대로 돌려줄 수 있습니다.
검사 예외(checked exception)까지 지원하려면 call()과 execute()의 throws 선언과 실패를 기록하는 범위를 함께 맞춰야 합니다. 이 call()에는 throws가 없으므로, 하위 구현에만 throws IOException을 붙이면 컴파일 오류가 납니다. IOException을 받는 별도 템플릿이 필요하다면 두 메서드 모두에 throws IOException을 선언하고 그 실패를 처리하게 만들 수 있으며, 함수형 인터페이스나 예외용 타입 파라미터가 반드시 필요한 것은 아닙니다. 다만 모든 호출자에게 검사 예외를 노출할지는 따로 검토합니다. 재정의할 때의 규칙은 JLS의 throws·재정의 규칙에서 확인할 수 있습니다.
익명 하위 클래스
다음 실행은 한 번의 게시글 조회를 익명 하위 클래스(anonymous subclass)로 감쌉니다. 기록기의 begin, success, failure가 예외를 던지지 않는다면, 대상이 던진 예외는 실패 로그를 남긴 뒤 같은 인스턴스로 다시 던져집니다.
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());
}
}
}call()이 post-41을 정상 반환하므로 출력은 시작, 성공, 결과 순서입니다. 앞의 두 줄은 execute가 call() 앞뒤에서 begin과 success를 호출해 템플릿 안에서 찍은 것이고, 마지막 줄은 execute가 돌려준 값을 main이 받아 출력한 것입니다. ConsoleSink는 걸린 시간을 출력하지 않으므로 실행할 때마다 같은 세 줄이 나옵니다.
BEGIN find post
END find post success
result=post-41익명 클래스가 여러 메서드에 반복되기 시작하면, 람다를 받는 콜백 구조가 더 간결합니다. 템플릿 메서드는 알고리즘의 단계마다 재정의가 필요하거나, 하위 클래스 타입 자체가 의미 있을 때 어울립니다.
예외와 정리의 불변식
성공 로그를 finally에 두면 실패도 성공으로 기록됩니다. 그래서 예외 처리 블록에서 실패를 기록하고 원래 예외를 다시 던집니다. 정리가 필요하다면 별도의 finally를 두되, 정리 중에 난 예외가 원래의 업무 실패를 덮지 않도록 억제된 예외(suppressed exception)로 연결합니다.
추적기가 예외 메시지 전체를 출력하면 인증 정보나 SQL 파라미터가 새어 나갈 수 있습니다. 기록기는 실패 클래스와 안정적인 오류 코드를 받고, 스택 트레이스를 남길지는 요청 단위의 로깅 정책이 정합니다. 템플릿이 로깅 백엔드 설정까지 맡지는 않습니다. 작업이 null을 반환했을 때 성공으로 볼지, 선택적 결과를 요구할지도 규칙으로 적어 둡니다. 템플릿이 null을 임의로 실패로 바꾸면 대상 작업의 의미를 바꾸게 됩니다.
지금의 execute가 실제로 보장하는 범위는 기록기가 실패하지 않는다는 조건에 기대고 있습니다. sink.begin은 try 밖에 있으므로 begin이 실패하면 call()도 실행되지 않고 catch에도 들어가지 않습니다. call()이 성공했는데 sink.success가 RuntimeException을 던지면, catch가 그 기록기의 실패를 sink.failure에 넘깁니다. call()이 실패 E를 던진 뒤 sink.failure가 다른 실패 L을 던지면, 마지막 throw failure에 닿지 못하고 L이 호출자에게 나갑니다. Error도 이 catch의 대상이라는 점은 같습니다.
따라서 업무 실패를 그대로 전달한다는 목표와, 이 예제가 보장하는 조건을 구분해야 합니다. 이 코드는 finally나 억제된 예외 연결을 구현하지 않았습니다. 기록기의 실패를 격리하거나 정리 단계를 더할 때는 업무 실패를 어떻게 보존할지 따로 정합니다.
상속으로 번지는 변경
하위 클래스는 부모의 호출 순서와 protected API를 알아야 합니다. 생성자 의존성이 늘거나 훅(hook)의 순서가 바뀌면 모든 하위 클래스가 영향을 받습니다. 자바는 단일 상속이므로, 이미 다른 기반 클래스를 상속한 컴포넌트에는 적용하기도 어렵습니다.
상속을 실제 도메인의 하위 타입 관계로 오해하지 않습니다. PostRepository extends TraceTemplate은 리포지토리가 트레이스 템플릿의 한 종류라는 모델이 아니라, 코드를 재사용하려는 관계일 뿐입니다. 다음 글에서 볼 객체 조합 기반의 전략과 콜백은 이 결합을 줄입니다.
final 템플릿 메서드는 순서를 보호하지만, 프록시가 하위 클래스를 만들어 메서드를 가로채야 하는 환경과는 충돌할 수 있습니다. 이 템플릿 객체 자체를 Spring AOP의 대상으로 삼을지, 내부 도우미로만 쓸지를 구분합니다.
호출 순서와 예외 검증
테스트용 기록기에 이벤트를 모아 BEGIN과 END의 순서를 확인하고, 대상이 던진 실패와 호출자가 받은 객체가 같은 인스턴스인지 봅니다. 지속 시간 숫자는 가짜 시간 제공자를 주입해 결정적으로 만듭니다. 콘솔 문자열 전체를 스냅샷으로 비교하면 핵심 순서보다 포매팅 변화에 더 쉽게 깨집니다.
템플릿 메서드를 고르는 기준
공통 순서가 안정적이고, 변형 지점이 몇 개뿐이며, 하위 클래스가 자연스러운 이름을 가질 때 템플릿 메서드가 읽기 좋습니다. 런타임에 작업을 바꾸거나 조합하고 싶고 변형이 람다 하나로 표현된다면 콜백이 낫습니다. 공통 코드가 두 줄뿐이라면 추상화가 오히려 코드를 따라가는 비용을 늘릴 수 있습니다.
더 깊이: Spring이 템플릿 메서드를 고른 곳
앞 장들에서 쓴 클래스 가운데에도 이 패턴으로 만들어진 것이 있습니다. Spring Framework 7.0.10의 AbstractPlatformTransactionManager는 getTransaction, commit, rollback을 final로 두고, 기존 트랜잭션이 있는지 판단하기, 전파 규칙 적용, 일시 정지와 재개, 커밋할 때 롤백 전용 표시 확인, 동기화 콜백 호출 같은 공통 흐름을 그 안에 고정합니다. 하위 클래스는 doGetTransaction, doBegin, doCommit, doRollback 같은 protected abstract 훅만 구현하며, 클래스 설명도 이것들을 템플릿 메서드라고 부릅니다. 그래서 DataSourceTransactionManager와 JpaTransactionManager는 연결이나 EntityManager를 다루는 부분만 다르고, 앞 장에서 본 전파와 롤백 전용의 의미는 똑같이 따릅니다.
서블릿 필터 글의 OncePerRequestFilter도 같은 구조입니다. doFilter가 이미 실행했는지 표시하는 속성과 디스패치 유형을 먼저 검사한 뒤 protected abstract인 doFilterInternal을 부르고, 하위 클래스는 보통 이 훅만 구현합니다. 다만 7.0.10의 doFilter는 final이 아니어서, 순서를 지키는 것은 컴파일러가 아니라 재정의하지 않는다는 관례에 맡겨져 있습니다. 두 예 모두 바뀌면 안 되는 규칙이 많고, 하위 클래스가 각 자원의 이름을 가진 의미 있는 타입이라는 점에서 이 글이 말한 기준에 맞습니다. 반대로 JdbcTemplate이나 TransactionTemplate은 이름과 달리 상속이 아니라 콜백을 받는 구조인데, 이는 다음 글에서 다룹니다.
정리
- 템플릿 메서드는
final메서드로 실행 순서를 고정하고,protected abstract훅 하나로 달라지는 작업만 받습니다. - 성공 기록은
finally가 아니라 정상 반환 경로에 두고, 실패는 기록한 뒤 같은 객체를 다시 던집니다. - 이 예제의 재전파 보장은 기록기가 실패하지 않는다는 조건에 기대며, 기록기 실패 격리와 억제된 예외 연결은 따로 구현해야 합니다.
- 상속은 하위 클래스를 부모의 훅 순서와 생성자에 묶으므로, 변형이 작업 하나라면 콜백이 더 가볍습니다.
연습 문제
1. 한 번의 시도만 추적하는 템플릿
재시도를 템플릿의 단계로 넣지 말고, 템플릿은 한 번의 시도만 추적하게 유지하세요. 호출자가 재시도할 때 각 시도와 전체 작업을 구별하는 이벤트 모델을 설계하고, 실패 객체의 동일성이 보존되는지 확인하는 테스트를 작성하세요.
해설 보기
시도 템플릿과 재시도 조정자(retry coordinator)를 분리하면 트레이싱 코드가 업무의 재시도 정책을 떠안지 않습니다.
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);
}
}실제 재시도에는 멱등성(idempotency), 일시적 실패의 분류, 기한, 백오프(backoff)가 더해져야 합니다. 템플릿은 각 시도를 기록하는 일만 합니다.
2. 마지막 실패와 시도 횟수 0
연습 1의 RetryCoordinator에서 maximumAttempts가 2입니다. 첫 시도는 런타임 예외 객체 E1을, 둘째 시도는 다른 런타임 예외 객체 E2를 던집니다. 실행되는 시도 번호와 호출자에게 최종 전파되는 객체를 구하세요. maximumAttempts가 0인 별도 호출에서는 시도가 몇 번 실행되고 어떤 실패가 나는지 코드를 그대로 따라가세요.
해설 보기
양수인 경우에는 attempt.run(1)과 attempt.run(2)를 실행하고, last를 E1에서 E2로 바꾼 뒤 E2 자체를 던집니다. 각 시도를 TraceTemplate으로 감쌌고 기록기가 실패하지 않는다면, 시작 기록과 실패 기록도 각각 두 번 남습니다.
0이면 반복문을 한 번도 실행하지 않고 null인 last를 throw하므로 NullPointerException이 납니다. 이 최소 조정자를 쓰려면 최대 시도 수가 1 이상이라는 입력 조건을 두거나 따로 검증해야 합니다. 모든 RuntimeException을 무조건 재시도하는 이 구조는 업무에 안전한 재시도 정책을 완성한 구현이 아닙니다.
3. 두 번의 시도와 두 번의 업무 효과
RetryCoordinator를 최대 2회로 실행하고 각 시도를 TraceTemplate으로 감쌉니다. 기록기의 세 메서드는 실패하지 않습니다. 교육용 메모리 저장소의 주문 생성 횟수는 처음에 0이며, call()은 호출될 때마다 이 횟수를 1 늘립니다. 첫 시도는 이 변경을 마친 뒤 응답을 만들다 런타임 예외 E를 던지고, 둘째 시도는 post-41을 반환합니다. 롤백과 중복 요청 검사는 없고 동시 호출도 없습니다.
call()의 호출 수, 최종 생성 횟수, 호출자가 받는 결과, 시도별 이벤트 순서를 구하세요. 성공 로그가 하나라면 논리적인 주문도 반드시 하나인가요?
해설 보기
call()은 2번 실행되고 생성 횟수도 2입니다. 이벤트는 첫 시도의 시작과 실패 기록, 둘째 시도의 시작과 성공 기록 순이며, 호출자는 post-41을 받습니다.
한 시도가 예외로 끝났다고 해서 그 전에 마친 업무 효과가 취소되지는 않습니다. 템플릿은 기록 순서를 고정할 뿐 롤백이나 멱등성을 제공하지 않으므로, 성공 기록 1개만으로 논리적인 주문이 1건이라고 말할 수 없습니다. 실제 조정자는 재시도해도 되는 실패인지, 기한과 백오프는 어떤지를 판단하고, 같은 요청의 중복 효과를 막는 업무 규칙을 따로 마련해야 합니다. 예를 들어 요청 키별 결과 기록과 중복 판정을 원자적으로 처리하는 규칙이 있어야 두 번째 시도가 첫 저장의 결과를 다시 쓸 수 있습니다. 두 시도에 같은 트레이스 ID를 붙이는 것만으로는 중복 저장이 막히지 않습니다.
다음 글 전략·템플릿 콜백에서는 상속 대신 객체 조합으로 달라지는 작업을 넘기는 전략 패턴과, 그 작업을 람다로 전달하는 템플릿 콜백 패턴을 다룹니다.