트랜잭션 관리 → MSA에서 데이터 일관성을 보장하는 방법 SAGA (Orchestrator) - 테코톡 2차 자료조사
- Saga 코레그로피 정의 및 개념
- 코레그래피의 장단점
- 활용 주문을 예를 들어서
- 코드로 표현 (예제 중하나)
- 시각화
- 마지막 비교 ( Saga1 vs Saga2)
혹시 모르니 추가
마이크로서비스의 독립적인 분산 트랜잭션 처리를 지원하는 패턴이 바로 사가(Saga) 패턴이다.
사가 패턴은 각 서비스의 로컬 트랜잭션을 순차적으로 처리하는 패턴이다. 사가 패턴은 여러개의 분산된 서비스를 하나의 트랜잭션으로 묶지 않고 각 로컬 트랜잭션과 보상 트랜잭션을 설정해 비즈니스 및 데이터 정합성을 맞춘다.
즉 로컬 트랜잭션은 자신의 데이터를 업데이트 한 다음 사가 내에 다음 로컬 트랜잭션을 업데이트 하는 트리거 메시지를 게시해서 정합성을 맞춘다.
다른 트랜잭션이 실패해서 롤백이 필요한 경우에 보상 트랜잭션을 따라서 앞서 처리한 트랜잭션들을 되돌리게 한다.
즉 일관성 유지가 필요한 트랜잭션들을 하나로 묶어서 처리하는게 아니라 하나하나씩 로컬 트랜잭션으로 처리하고 이벤트를 날리고 다음 로컬 트랜잭션이 처리하고 이런 식으로 진행된다. 실패하면 이때까지 처리했던 트랜잭션들을 모두 되돌리고.마이크로서비스의 독립적인 분산 트랜잭션 처리를 지원하는 패턴이 바로 사가(Saga) 패턴이다.
사가 패턴은 각 서비스의 로컬 트랜잭션을 순차적으로 처리하는 패턴이다. 사가 패턴은 여러개의 분산된 서비스를 하나의 트랜잭션으로 묶지 않고 각 로컬 트랜잭션과 보상 트랜잭션을 설정해 비즈니스 및 데이터 정합성을 맞춘다.
즉 로컬 트랜잭션은 자신의 데이터를 업데이트 한 다음 사가 내에 다음 로컬 트랜잭션을 업데이트 하는 트리거 메시지를 게시해서 정합성을 맞춘다.
다른 트랜잭션이 실패해서 롤백이 필요한 경우에 보상 트랜잭션을 따라서 앞서 처리한 트랜잭션들을 되돌리게 한다.
즉 일관성 유지가 필요한 트랜잭션들을 하나로 묶어서 처리하는게 아니라 하나하나씩 로컬 트랜잭션으로 처리하고 이벤트를 날리고 다음 로컬 트랜잭션이 처리하고 이런 식으로 진행된다. 실패하면 이때까지 처리했던 트랜잭션들을 모두 되돌리고.
결과적 일관성(eventual consistency)
모든 비즈니스에서 데이터는 일관성이 있어야 한다. 하지만 이전까지는 이 같은 데이터 일관성은 실시간으로 반드시 맞아야 한다는 생각이다.
그렇지만 모든 비즈니스 규칙들이 실시간으로 데이터 일관성이 맞아야 하지는 않다. 예를 들면 쇼핑몰에서 주문을 하고 결제 처리가 완료 되면 결제 내용과 함께 주문 내역이 고객의 이메일로 전송돼야 한다고 생각해보자.
이 경우들을 모두 그 즉시 순차적으로 처리해야 하지는 않다. 주문자가 폭등한 경우를 생각해보면 된다. 이 경우에 주문자가 많아져서 결제에도 그 트래픽이 전파되서 결제 서비스에 장애가 발생할 수 있다. 주문 서비스를 Scale Out 한다고 문제가 해결되지 않는다.
하지만 주문만 미리 받아놓고 (주문 서비스만 미리 Scale Out 해놓는다면) 외부 결제 서비스는 자신이 처리할 수 있는 만큼만 계속해서 처리한다라고 가정을 해봐도 문제되는 점은 없다. 데이터의 일관성이 실시간으로 맞지 않더라도 어느 일정 시점에서는 일관성이 맞을 것이다. 이를 결과적 일관성이라고 한다.
이런 결과적 일관성은 고가용성을 극대화 한다. 실시간성을 강제로 해서 다른 서비스의 가용성을 떨어뜨리지 않는다.
이는 마이크로서비스의 사가 패턴과 이벤트 메시지 기반 비동기 통신을 통해서 만들 수 있다.
choreography-based pattern 개념
Example choreography-based pattern
choreography-based pattern 은 Saga에 참여하는 사람들이 이벤트를 교환하고 협력하는 방식입니다.
오케스트레이션과 달리 중앙 제어자가 없이 각 서비스가 이벤트를 발행하고, 이를 구독한 다른 서비스가 이어서 동작하는 방식입니다. 서비스 간 결합도가 낮고, SPOF(Single Point of Failure)가 없습니다.
즉, 중앙 조정자 없이 서비스 간 이벤트 기반으로 흐름을 조율할 수 있습니다.
SPOF
시스템 구성 요소 중 하나가 실패했을 때 전체 시스템이 영향을 받아 동작을 멈추는 지점
즉, 하나만 고장 나도 전체 서비스가 중단되는 부분
Choreography SAGA (코레오그래피 방식)
- 각 서비스가 이벤트 기반으로 트랜잭션을 처리
- 서비스 간 직접 이벤트를 발행하고 구독하여 비동기적으로 실행
- 오케스트레이터 없이 서비스들이 자율적으로 트랜잭션을 처리.
- 각 서비스는 다른 서비스에 이벤트를 전송해, 자신이 어떤 작업 완료했는지 알리고, 실패시 보상 트랜잭션 처리.
2. 코레오그래피 방식의 장단점
장점
- SPOF가 없음: 중앙 오케스트레이터가 없어 장애에 강하고, 시스템 간 결합도가 낮음
- 확장성 우수: 새로운 서비스가 쉽게 추가될 수 있음
- 서비스 간 결합도 낮음: 독립적으로 배포와 변경 가능
단점
- 비즈니스 흐름 파악 어려움: 이벤트 기반 흐름이 분산되어 있어서 복잡해질 수 있음
- 디버깅/모니터링 어려움: 장애 발생시, 전체 프로세스 추적이 어렵기 때문에 추적을 위한 도구가 필요함
- 순서, 중복 처리 이슈: 이벤트의 순서 보장과 중복 방지가 복잡
코레오그래피 방식에는 두 가지 주요 구현 전략
- 이벤트 발행 및 구독 방식** (Event Publish & Subscribe)**
- Outbox 패턴 기반 이벤트 처리
Correlation ID와 Outbox 패턴의 개념
Correlation ID
- 각 이벤트 메시지에 orderId, transactionId 같은 고유 식별자를 포함하여 트랜잭션 흐름을 추적할 수 있게 합니다.
- 분산 환경에서는 각 서비스가 독립적이기 때문에, 어떤 요청에서 발생한 이벤트인지 식별이 어렵습니다. Correlation ID를 통해 이를 해결합니다.
Outbox 패턴
- 이벤트 발행과 DB 트랜잭션을 원자적으로 처리하기 위한 전략입니다.
- 서비스는 먼저 이벤트를 Outbox 테이블(DB)에 저장하고, 별도의 프로세스가 이 이벤트를 메시지 브로커(Kafka 등)로 발행합니다.
- 이 방식은 "DB에는 반영됐는데 이벤트는 발행되지 않은 유실 문제를 방지합니다.
Choreography 방식의 이벤트 처리 비교
| 항목 | 이벤트 발행 /구독 | Outbox 패턴 기반 |
| 트랜잭션 안정성 | 메시지 브로커 전송 시점과 DB 커밋이 분리되어 이벤트 유실 가능성 존재 | DB 트랜잭션 내에서 이벤트 저장 → 트랜잭션 일관성 보장 |
| 시스템 구성 난이도 | 브로커에 직접 이벤트 전송 → 구현 구조가 비교적 단순하고 빠르게 적용 가능 | Outbox 테이블, Poller, 중복 방지 로직 등 구성 요소 및 연동 복잡도 증가 |
| 운영 관리 항목 | 브로커 상태만 주로 관리하면 됨 → 운영 포인트가 적음 | Outbox 테이블 상태, 재전송, 중복 처리 등 운영 고려사항과 처리 포인트 다수 |
| 이벤트 추적 가능성 | Correlation ID 등 메타데이터 필요 → 추적 구현 필요성 존재 | 상태 필드, 로그 테이블 기반 → 이벤트 흐름 추적 및 장애 분석 용이 |
활용 예시 - 주문 처리 시나리오
시나리오:
- 고객이 주문을 생성
- 결제 서비스가 결제 수행
- 재고 서비스가 상품 재고 감소
- 배송 서비스가 배송 요청
각 단계는 이벤트 기반으로 이어집니다.
활용 시나리오 정리 - 주문 처리 흐름
성공 시나리오
- OrderService: 주문 생성 후 OrderCreated 이벤트 발행
- PaymentService: 이벤트 구독 후 결제 진행, 성공 시 PaymentCompleted 이벤트 발행
- InventoryService: 이벤트 구독 후 재고 차감, 성공 시 StockReduced 이벤트 발행
- ShippingService: 이벤트 구독 후 배송 시작, ShippingStarted 이벤트 발행
실패 및 보상 시나리오 (예: 결제 실패)
- PaymentService: 결제 실패 시 PaymentFailed 이벤트 발행
- OrderService: 이를 수신하고 주문 상태를 CANCELLED로 변경
- InventoryService: 보상 트랜잭션으로 재고 복원 이벤트 발행
코드 예제 (Spring Boot, Kafka 기반 예시)
📦 OrderService - 주문 생성 & Outbox에 이벤트 기록
@Transactional
public Order createOrder(String productName, int quantity, int price) {
Order order = new Order(productName, quantity, price);
orderRepository.save(order);
// 주문 생성 이벤트를 Outbox 테이블에 저장
OrderCreatedEvent event = new OrderCreatedEvent(order.getId(), price * quantity);
outboxRepository.save(new OutboxEvent("order-created", event));
return order;
}
💳 PaymentService - 주문 생성 이벤트 구독 & 결제 처리 후 Outbox에 이벤트 기록
@KafkaListener(topics = "order-created")
public void handleOrderCreated(OrderCreatedEvent event) {
boolean success = paymentGateway.process(event.getOrderId());
if (success) {
// 결제 성공 이벤트 Outbox에 저장
PaymentCompletedEvent successEvent = new PaymentCompletedEvent(event.getOrderId());
outboxRepository.save(new OutboxEvent("payment-completed", successEvent));
} else {
// 결제 실패 이벤트 Outbox에 저장
PaymentFailedEvent failEvent = new PaymentFailedEvent(event.getOrderId(), "카드 한도 초과");
outboxRepository.save(new OutboxEvent("payment-failed", failEvent));
}
}
Outbox 패턴과 Correlation ID 사용이 권장됩니다:
- DB 변경과 이벤트 발행을 하나의 트랜잭션으로 묶음
- 이벤트마다 orderId와 같은 식별자를 포함하여 추적 가능하게 함
🏬 InventoryService - 결제 성공 이벤트 구독 & 재고 차감 후 Outbox에 이벤트 기록
@KafkaListener(topics = "payment-completed")
public void handlePaymentCompleted(PaymentCompletedEvent event) {
inventoryService.reduceStock(event.getOrderId());
// 재고 차감 완료 이벤트 Outbox 저장
StockReducedEvent stockEvent = new StockReducedEvent(event.getOrderId());
outboxRepository.save(new OutboxEvent("stock-reduced", stockEvent));
}
🚚 ShippingService - 재고 차감 이벤트 구독 & 배송 시작 후 Outbox에 이벤트 기록
@KafkaListener(topics = "stock-reduced")
public void handleStockReduced(StockReducedEvent event) {
shippingService.arrangeShipping(event.getOrderId());
// 배송 시작 이벤트 Outbox 저장
ShippingStartedEvent shippingEvent = new ShippingStartedEvent(event.getOrderId());
outboxRepository.save(new OutboxEvent("shipping-started", shippingEvent));
}
시각화
[Order Service] ---> (OrderCreated Event)
|
v
[Payment Service] ---> (PaymentCompleted / Failed Event)
|
v
[Inventory Service] ---> (StockReduced Event)
|
v
[Shipping Service] ---> (ShippingStarted Event)
상 트랜잭션 시:
- 예: 결제 실패 → 주문 상태 변경 + 재고 복원 + 알림 전송
Saga 패턴 오케스트레이션 vs 코레오그래피 비교
| 항목 | Saga - 오케스트레이션 | Saga - 코레오그래피 |
| 조정 방식 | 중앙 오케스트레이터 | 이벤트 발행/구독 |
| 결합도 | 서비스 ↔ 오케스트레이터 | 서비스 간 느슨한 결합 |
| 장애 전파 | Orchestrator 장애 시 전체 영향 | SPOF 없음 |
| 확장성 | 중앙 관리 부담 증가 | 신규 서비스 쉽게 추가 |
| 오류 처리 | 중앙에서 일괄 보상 트리거 | 이벤트 기반 개별 처리 |
선택 가이드:
- 단순한 구조: 코레오그래피
- 복잡한 흐름, 제어 필요: 오케스트레이션
참고: 실무 적용 팁
- Outbox 패턴 필수 (이벤트 유실 방지)
- 보상 트랜잭션의 멱등성 설계 필요
- DLQ, Retry, Monitoring 체계 마련
- Zipkin, Jaeger로 이벤트 흐름 추적
- Kafka 사용 시 파티션, offset 관리 필요