트랜잭션 내 외부 API 호출: DB 커넥션 풀 고갈

mseo39·2025년 12월 9일

TIL

목록 보기
17/19
post-thumbnail

현재 코드는 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)
  • 히스토리

트러블 슈팅: AOP와 Self-Invocation

롤백 영역을 같은 클래스 내의 별도 메서드로 분리하여 @Transactional을 적용하려 했으나 Spring AOP의 프록시 동작 방식 때문에 내부 호출(Self-Invocation) 시 트랜잭션이 적용되지 않는 문제가 발생했습니다

Self-Invocation 문제란?
AOP는 프록시를 통해 동작합니다.
외부에서 호출할 때는 프록시를 거치지만 클래스 내부에서 자신의 메서드(this.method())를 호출할 때는 프록시를 거치지 않고 원본 객체를 바로 호출하므로 트랜잭션이 적용되지 않는 현상입니다

해결 솔루션: TransactionTemplate

자기 자신을 주입받는 방법이나 별도 클래스로 분리하는 방법도 있지만

  • 비즈니스 로직의 응집도를 유지하면서
  • 불필요한 클래스 생성을 막고
  • 코드 레벨에서 트랜잭션 범위를 명시적으로 보여주기 위해

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);
        });
    }
    // ... 실패 처리 로직
}
profile
하루하루 성실하게

0개의 댓글