
현재 코드는 createTemplate 메서드 전체에 @Transactional이 걸려 있기 때문에 외부 API 호출이 진행되는 동안 실제 DB 작업이 없음에도 DB 커넥션을 계속 점유하는 상태가 됩니다
@Transactional // (1) 트랜잭션 컨텍스트 시작
public TemplateCreationResult createTemplate(Long userId, TemplateCreateRequest request) {
// (2) DB 작업 발생 (INSERT)
// 여기서 HikariCP에서 물리적인 커넥션을 획득
UserTemplateRequest userRequest = userTemplateRequestService.createInitialRequest(...);
// (3) 외부 API 호출 (Blocking)
// 위 (2)번에서 획득한 커넥션이 여기서 대기
// 왜냐? (1)번 트랜잭션이 아직 안 끝났기 때문
ResponseEntity<...> responseEntity = aiApiClient.createTemplate(userRequest);
// ... 이후 로직
}
상황: 동시 접속자 50명이
createTemplate API를 호출함
조건: 외부 API 응답 시간 10초 / DB 커넥션 풀 크기 10개
커넥션 고갈: 선발대 10명의 요청이 createInitialRequest(DB 저장)를 수행하며 DB 커넥션 10개를 모두 선점
병목: 이들은 커넥션을 가지고 외부 API 응답을 10초간 대기 (DB는 놀고 있지만 커넥션은 반납X)
타임 아웃: 이후 40명의 요청은 커넥션이 없어 대기열에서 기다리다 결국 대기 시간(30초)을 초과하여 줄줄이 에러를 발생
해결 방법은 외부 API 호출과 같은 긴 작업은 트랜잭션 범위 밖으로 빼내고 DB 작업이 필요한 순간에만 트랜잭션을 여는 것입니다
그래서 먼저 롤백되어야 하는 영역과 보존되어야 하는 영역을 구분해주고 트랜잭션 분리하는 리팩토링을 진행했습니다

✅ 보존 영역 / REQUIRES_NEW
메인 로직이 실패하더라도 "시도했다"는 사실과 "에러 원인"은 남아야 추적 및 통계가 가능함
PENDING)FAILED)❌ 롤백 영역 / REQUIRED
데이터 정합성 유지를 위함 -> 템플릿은 저장됐는데 상태가 여전히 PENDING이면 데이터 불일치가 발생하므로 실패 시 데이터가 사라져야 함
Entity)COMPLETED)롤백 영역을 같은 클래스 내의 별도 메서드로 분리하여 @Transactional을 적용하려 했으나 Spring AOP의 프록시 동작 방식 때문에 내부 호출(Self-Invocation) 시 트랜잭션이 적용되지 않는 문제가 발생했습니다
Self-Invocation 문제란?
AOP는 프록시를 통해 동작합니다.
외부에서 호출할 때는 프록시를 거치지만 클래스 내부에서 자신의 메서드(this.method())를 호출할 때는 프록시를 거치지 않고 원본 객체를 바로 호출하므로 트랜잭션이 적용되지 않는 현상입니다
자기 자신을 주입받는 방법이나 별도 클래스로 분리하는 방법도 있지만
TransactionTemplate을 선택했습니다
@Transactional은 프록시를 통해 동작하기 때문에 내부 호출 시 무시되지만 TransactionTemplate은 프록시를 거치지 않고 개발자가 코드 내에서 직접 트랜잭션 매니저를 호출하여 범위를 제어하기 때문에 Self-Invocation 문제에서 자유롭습니다
적용코드 예시
// ✅ 리팩토링 후: 외부 API는 트랜잭션 밖에서(No-TX), DB 저장만 트랜잭션 안에서(TX)
public TemplateCreationResult createTemplate(...) {
// 1. [전처리] 초기 기록 (별도 트랜잭션)
var userRequest = userTemplateRequestService.createInitialRequest(...);
// 2. [No-TX] 외부 API 호출 (Blocking 구간 - DB 커넥션 점유 X)
var response = aiApiClient.createTemplate(userRequest);
if (response.isSuccess()) {
// 3. [TX] 성공 처리 (TransactionTemplate 사용)
// 여기서만 짧게 커넥션을 점유하고 바로 반납!
return transactionTemplate.execute(status -> {
Template template = templateFactory.createFrom(...);
saveTemplateHistory(template);
userTemplateRequestService.markAsCompleted(request.getId());
return new TemplateCreationResult.Complete(template);
});
}
// ... 실패 처리 로직
}