본문으로 건너뛰기
Advertisement

실전 고수 팁 — 분산 환경에서의 보상 트랜잭션(Saga 패턴) 맛보기

일체형 서버(Monolithic) 환경에서는 스프링의 @Transactional 애너테이션 하나만 쓰면 모든 취소(Rollback) 처리가 완벽했습니다. 결제 DB와 주문 DB, 쿠폰 DB가 모두 하나의 서버 안에 묶여 연결되어 있었기 때문입니다.

1. 뼈아픈 마이크로서비스(MSA) 분산 트랜잭션의 한계

MSA 환경에서는 완전히 다른 서버 인스턴스(결제 서버, 포인트 서버, 배송 서버)들이 각자의 독립적인 DB를 사용합니다.

  • 결제 서버에 POST /pay API를 쏘고 (성공)
  • 포인트 서버에 POST /point/deduct API를 쐈는데 (성공)
  • 배송 서버에 POST /delivery API를 쐈다가 터졌습니다 (실패)

이때 "앗, 실패했으니 앞서 성공했던 결제와 포인트를 모두 롤백(Rollback)시켜줘!" 라고 외쳐도, 이미 결제 서버와 포인트 서버의 DB는 각자의 독립적인 트랜잭션을 끝내고 데이터베이스에 영구적인 Commit을 찍어버렸습니다. 서로 남의 서버이므로 @Transactional 롤백이 아예 먹히지 않습니다.

2. 해결책: 보상 트랜잭션과 Saga 패턴 (Saga Pattern)

글로벌 2PC(Two-Phase Commit) 등 무거운 락킹 메커니즘을 배제하고, 실무 애플리케이션 계층에서 가장 보편적으로 쓰이는 해결책이 바로 이벤트 기반 Saga 패턴입니다.

사라진 롤백 스위치 대신, "기존 성공 연산을 반대로 되돌리는 새로운 취소 연산(보상 트랜잭션)"을 수동으로 순차 발사하는 것입니다.

  1. 배송 서버 실패 (이벤트 발행: DeliveryFailedEvent)
  2. 중앙 오케스트레이터 서버 (혹은 카프카 브로커)가 이를 캐치
  3. 포인트 서버에게 POST /point/restore (차감된 포인트 다시 + 환불해주기) API를 호출
  4. 결제 서버에게 POST /pay/cancel (결제액 PG망 취소 쏘기) API를 호출

💡 실무 설계 원칙

단일 메서드 내에 HTTP 클라이언트(RestClient) 로직과 DB 처리 로직을 무조건 분리해야 합니다. DB 트랜잭션 안에서 30초짜리 외부 서버 취소 API를 동기식으로 기다리게 설계하면 톰캣 스레드 풀이 터집니다. 트랜잭션을 먼저 끊고, @Async나 메세지 큐(Kafka/RabbitMQ)를 이용해 비동기적으로 취소 요청을 날려야 결제 아키텍처가 버팁니다.

Advertisement