Saga 패턴의 핵심 개념
분산 시스템에서 2PC(2-Phase Commit)는 성능과 가용성 문제로 마이크로서비스에 적합하지 않습니다. Saga 패턴은 각 서비스의 로컬 트랜잭션을 이벤트로 연결하고, 실패 시 이전 단계를 보상(취소)하는 방식으로 최종 일관성을 달성합니다.
e-커머스 주문 처리 Saga
주문 → 재고 예약 → 결제 → 배송 예약의 단계에서 어느 단계든 실패하면 이전 단계들이 자동으로 취소됩니다.
from("direct:placeOrder")
.saga()
.timeout(Duration.ofMinutes(5)) // 5분 내 완료되지 않으면 보상
.compensation("direct:cancelOrder") // 실패 시 실행할 보상 라우트
.option("orderId", header("orderId"))
.to("direct:reserveInventory") // 1단계: 재고 예약
.to("direct:processPayment") // 2단계: 결제
.to("direct:arrangeShipping") // 3단계: 배송 예약
.end()
.log("주문 완료: ${header.orderId}");
보상 라우트 구현
// 주문 취소 보상 라우트
from("direct:cancelOrder")
.log("주문 취소 보상 시작: ${header.orderId}")
.multicast()
.to("direct:releaseInventory") // 재고 예약 해제
.to("direct:refundPayment") // 결제 취소
.to("direct:cancelShipping") // 배송 취소
.end()
.log("주문 취소 완료: ${header.orderId}");
// 각 단계도 보상 라우트 가짐
from("direct:reserveInventory")
.saga()
.propagation(SagaPropagation.MANDATORY)
.compensation("direct:releaseInventory")
.to("bean:inventoryService?method=reserve")
.end();
Saga 상태 영속화
Saga가 진행 중에 서버가 재시작되면 상태를 잃지 않아야 합니다. Camel Saga는 상태 저장소를 설정할 수 있습니다.
// DB 기반 Saga 저장소
JdbcSagaRepository sagaRepository = new JdbcSagaRepository(dataSource);
context.addService(sagaRepository);
// 또는 InMemory (테스트용)
InMemorySagaService sagaService = new InMemorySagaService();
context.addService(sagaService);
타임아웃과 멱등성 처리
from("direct:processPaymentStep")
.saga()
.propagation(SagaPropagation.MANDATORY)
.option("sagaId", exchangeProperty(Exchange.SAGA_LONG_RUNNING_ACTION))
.compensation("direct:refundPayment")
.idempotentConsumer(
exchangeProperty(Exchange.SAGA_LONG_RUNNING_ACTION),
MemoryIdempotentRepository.memoryIdempotentRepository(1000))
.process(exchange -> {
// 멱등 처리: 같은 sagaId로 재시도해도 중복 결제 안 됨
paymentService.charge(exchange.getIn().getBody(Order.class));
})
.end();
Saga vs 2PC 선택 기준
- Saga 적합: 마이크로서비스, 높은 가용성, 긴 실행 트랜잭션, 최종 일관성 허용
- 2PC 적합: 즉각적인 일관성 필수, 짧은 트랜잭션, 모든 참여자가 XA 지원
대부분의 현대 마이크로서비스 시스템에서는 Saga가 더 적합합니다. 단, UI에서 즉시 결과를 보여줘야 한다면 낙관적 UI 업데이트와 함께 사용합니다.