배경 및 목표

외부 API 호출이 트랜잭션 안에 묶여 있어, 외부 서버 응답이 느려지면 트랜잭션이 길어지고 DB 커넥션을 오래 점유했습니다. 외부 장애가 곧 자사 서비스 장애로 이어지는 구조였습니다.

graph LR
    subgraph before["❌ 기존: 하나의 트랜잭션"]
        TX["@Transactional"] --> DB["DB 저장"]
        TX --> EXT["외부 API 호출 (수 초)"]
        EXT -->|"장애/지연"| TX
    end
    TX --> CONN["DB 커넥션 오래 점유"]
    CONN --> FAIL["커넥션 풀 고갈 → 서비스 장애"]

핵심 문제는 외부 호출이 느리거나 실패하면 트랜잭션 전체가 영향을 받는다는 점이었습니다. DB 저장은 수 ms면 끝나지만, 외부 호출이 수 초~타임아웃까지 걸리면 그 동안 DB 커넥션이 점유됩니다.

목표

  • 외부 동기 호출과 DB 트랜잭션을 분리해 롱 트랜잭션을 없앤다.
  • 외부 장애가 자사 서비스로 전파되지 않도록 격리한다.

해결 방법과 해결 후보군

후보군 비교

방식설명한계
외부 호출을 트랜잭션 안에 유지@Transactional 안에서 외부 호출롱 트랜잭션, 외부 지연·장애가 DB 커넥션 점유·전파
이벤트/큐로 완전 비동기화커밋 후 메시지 발행, 별도 워커 처리인프라·정합성 복잡도 증가 (이 케이스엔 과함)
트랜잭션 경계 축소 + 커밋 후 동기 호출 (채택)DB 저장만 트랜잭션, 외부는 커밋 후 호출커넥션 즉시 반환 + 외부 장애 격리 + 구현 단순

1. 트랜잭션 경계를 DB 저장까지로 제한

문제의 근본은 트랜잭션 경계가 너무 넓다는 것이었습니다. 외부 API 호출은 DB 트랜잭션과 무관한 작업인데, 하나의 @Transactional 안에 묶여 있었습니다. 트랜잭션 경계를 DB 저장까지만으로 좁히고, 외부 호출은 트랜잭션이 끝난 뒤에 수행하도록 코드 구조를 분리했습니다.

// ❌ 기존: 하나의 트랜잭션에 모든 것이 묶임
@Transactional
fun process(request: CreateRequest) {
    entityRepository.save(request.toEntity())  // DB 저장
    externalClient.sync(request)               // 외부 호출 (수 초)
    // → 외부 호출 동안 트랜잭션 유지, 커넥션 점유
}
 
// ✅ 개선: 트랜잭션 경계 분리
fun process(request: CreateRequest) {
    val entity = saveEntity(request)      // 트랜잭션 안
    syncToExternal(entity)                // 트랜잭션 밖
}
 
@Transactional
fun saveEntity(request: CreateRequest): EntityA {
    return entityRepository.save(request.toEntity())
    // → 수 ms 만에 커밋, 커넥션 즉시 반환
}
 
fun syncToExternal(entity: EntityA) {
    externalClient.sync(entity)
    // → 느리거나 실패해도 DB 트랜잭션에 무영향
}
graph LR
    subgraph after["✅ 개선: 트랜잭션 경계 분리"]
        TX2["@Transactional"] --> DB2["DB 저장"]
    end
    TX2 -->|"트랜잭션 커밋 후"| EXT2["외부 API 동기 호출"]

2. 커밋 후 외부 호출 분리

트랜잭션을 담당하는 메서드와 외부 호출을 담당하는 메서드를 분리하여, 트랜잭션이 커밋된 뒤에 외부 API를 동기적으로 호출합니다.

  • 트랜잭션은 DB 저장만 수행 → 수 ms 만에 커넥션 반환
  • 외부 장애가 DB 커넥션 풀에 전파되지 않음
sequenceDiagram
    participant C as Client
    participant SVC as Service
    participant DB as Database
    participant EXT as 외부 API

    C->>SVC: 요청
    Note over SVC,DB: @Transactional 시작
    SVC->>DB: DB 저장
    Note over SVC,DB: @Transactional 커밋 → 커넥션 반환
    SVC->>EXT: 외부 API 동기 호출
    EXT-->>SVC: 응답
    SVC-->>C: 응답
    Note over EXT: 느리거나 실패해도<br/>DB 트랜잭션에 무영향

결과

지표기존개선
트랜잭션 시간외부 응답 포함 (수 초)DB 저장만 (수 ms)
커넥션 점유외부 호출 동안 유지커밋 즉시 반환
외부 장애 영향DB 커넥션 풀 전파트랜잭션에 무영향 (격리)

모니터링

  • 외부 호출 큐의 랙·실패율과 DB 트랜잭션 처리 시간을 관측한다.
  • 외부 장애 시 자사 서비스 안정성(격리 여부)을 관측한다.