[Daily morning study] Saga 패턴과 분산 트랜잭션
#daily morning study
분산 트랜잭션 문제
마이크로서비스 아키텍처에서 각 서비스는 자체 데이터베이스를 소유한다. 단일 비즈니스 트랜잭션이 여러 서비스에 걸쳐 있을 때 데이터 일관성을 유지하기가 어려워진다.
예를 들어 주문 처리 흐름:
- 주문 서비스 → 주문 생성
- 재고 서비스 → 재고 차감
- 결제 서비스 → 결제 처리
- 배송 서비스 → 배송 요청
이 중 하나가 실패하면? 모놀리식에서는 DB 트랜잭션으로 롤백하면 됐지만, MSA에서는 각 서비스가 이미 자체 DB에 커밋한 상태라 단순 롤백이 불가능하다.
2PC(Two-Phase Commit)의 한계
전통적인 분산 트랜잭션 해결책인 2PC는 두 단계로 동작한다.
- Prepare 단계: 코디네이터가 모든 참여자에게 “커밋 준비됐어?” 확인
- Commit 단계: 모두 OK면 커밋, 하나라도 NO면 롤백
하지만 MSA 환경에서는 여러 문제가 있다.
- 코디네이터가 SPOF(단일 장애 지점)
- 블로킹 프로토콜 → 락을 오래 보유해 성능 저하
- 네트워크 파티션 발생 시 불일치 가능
- 서비스 자율성을 해침
Saga 패턴
Saga는 분산 환경에서 데이터 일관성을 보장하는 패턴이다.
핵심 아이디어: 긴 트랜잭션을 여러 개의 로컬 트랜잭션으로 분해하고, 각 단계가 실패하면 이전에 성공한 트랜잭션을 취소하는 보상 트랜잭션(Compensating Transaction)을 실행한다.
완전한 롤백 대신 “이미 일어난 일을 의미적으로 되돌리는” 방식이다.
Saga 구현 방식
1. Choreography (코레오그래피)
각 서비스가 로컬 트랜잭션 완료 후 이벤트를 발행하고, 다음 서비스가 그 이벤트를 구독해서 처리한다. 중앙 오케스트레이터 없이 서비스끼리 이벤트로만 소통한다.
성공 흐름:
주문서비스 → OrderPlaced 이벤트 발행
재고서비스 → 구독 → 재고 차감 → StockReserved 이벤트 발행
결제서비스 → 구독 → 결제 처리 → PaymentCompleted 이벤트 발행
배송서비스 → 구독 → 배송 요청 → ShippingRequested 이벤트 발행
결제 실패 시 보상 흐름:
결제서비스 → PaymentFailed 이벤트 발행
재고서비스 → 구독 → 재고 복구 → StockReleased 이벤트 발행
주문서비스 → 구독 → 주문 취소
장점
- 서비스 간 직접 결합이 없음 (느슨한 결합)
- 중앙 코디네이터 없음 → 단일 장애 지점 없음
단점
- 전체 흐름을 한눈에 파악하기 어려움
- 서비스 간 순환 이벤트 의존이 생길 수 있음
- 디버깅과 추적이 복잡
2. Orchestration (오케스트레이션)
중앙 Saga Orchestrator가 각 서비스에 명령을 내리고 결과를 받아 다음 단계를 결정한다.
SagaOrchestrator
├─ OrderService.createOrder() → 성공
├─ InventoryService.reserveStock() → 성공
├─ PaymentService.processPayment() → 실패
├─ InventoryService.releaseStock() (보상 트랜잭션)
└─ OrderService.cancelOrder() (보상 트랜잭션)
class OrderSagaOrchestrator {
void execute(OrderCommand cmd) {
String orderId = orderService.createOrder(cmd);
try {
inventoryService.reserveStock(cmd.getItemId(), cmd.getQty());
try {
paymentService.processPayment(cmd.getPaymentInfo());
shippingService.requestShipping(cmd.getAddress());
} catch (PaymentFailedException e) {
inventoryService.releaseStock(cmd.getItemId(), cmd.getQty());
orderService.cancelOrder(orderId);
}
} catch (StockUnavailableException e) {
orderService.cancelOrder(orderId);
}
}
}
장점
- 전체 흐름을 오케스트레이터 하나에서 명확하게 파악 가능
- 복잡한 비즈니스 로직 관리 용이
- 개별 서비스는 단순하게 유지
단점
- 오케스트레이터가 복잡해질 수 있음
- 오케스트레이터와 각 서비스 간 결합도가 높아짐
보상 트랜잭션(Compensating Transaction)
Saga에서 실패 시 이전에 성공한 로컬 트랜잭션을 취소하는 작업이다.
중요한 특징:
- 물리적 롤백이 아니라 의미적(semantic) 롤백
- 이미 커밋된 데이터를 되돌리는 새로운 비즈니스 로직
- 모든 로컬 트랜잭션에 대한 보상 트랜잭션을 미리 설계해야 함
| 로컬 트랜잭션 | 보상 트랜잭션 |
|---|---|
| 주문 생성 | 주문 취소 |
| 재고 차감 | 재고 복구 |
| 결제 처리 | 환불 처리 |
| 배송 요청 | 배송 취소 |
멱등성(Idempotency)
네트워크 재시도나 메시지 큐의 중복 전달로 인해 같은 이벤트가 여러 번 처리될 수 있다. 동일한 요청이 여러 번 들어와도 결과가 같아야 한다.
void reserveStock(String sagaId, String itemId, int qty) {
if (reservationRepository.existsBySagaId(sagaId)) {
return; // 이미 처리된 요청 → 무시
}
stock.decrease(itemId, qty);
reservationRepository.save(sagaId, itemId, qty);
}
sagaId를 유니크 키로 저장해 중복 처리를 방지한다.
Saga vs 2PC 비교
| 항목 | 2PC | Saga |
|---|---|---|
| 일관성 수준 | 강한 일관성(ACID) | 최종 일관성(Eventual Consistency) |
| 성능 | 낮음 (락 보유) | 높음 |
| 가용성 | 코디네이터 장애 시 블로킹 | 고가용성 |
| 구현 복잡도 | 낮음 (DB 수준 지원) | 높음 (보상 트랜잭션 설계 필요) |
| MSA 적합성 | 낮음 | 높음 |
격리성 부재 문제
Saga는 ACID 중 격리성(Isolation) 이 없다. 각 로컬 트랜잭션이 커밋되는 순간 다른 Saga가 중간 상태의 데이터를 읽을 수 있다.
이를 해결하기 위한 전략:
- Semantic Lock: 처리 중인 레코드에 플래그를 설정해 다른 트랜잭션이 접근하지 못하게 함
- Commutative Update: 순서에 관계없이 결과가 같은 업데이트 설계
- Pessimistic View: 최악의 상태를 가정하고 UI/비즈니스 로직 설계
실제 구현 도구
- Eventuate Tram Saga: Spring 기반 Saga 프레임워크, Choreography/Orchestration 모두 지원
- AWS Step Functions: 오케스트레이션 기반 분산 워크플로우 서비스
- Temporal.io: 내구성 있는 워크플로우 실행 플랫폼
- Apache Camel: Saga EIP(Enterprise Integration Pattern) 지원
핵심 정리
- 분산 환경에서 2PC 대신 Saga로 데이터 일관성 확보
- 로컬 트랜잭션 + 보상 트랜잭션 쌍으로 설계
- 코레오그래피(이벤트 기반, 느슨한 결합) vs 오케스트레이션(중앙 제어, 명확한 흐름)
- 멱등성 처리 필수
- 격리성 없음 → 최종 일관성(Eventual Consistency) 모델 수용