배경 및 목표
외부 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 트랜잭션 처리 시간을 관측한다.
- 외부 장애 시 자사 서비스 안정성(격리 여부)을 관측한다.