분산 시스템에서는 하나의 서비스가 독립적으로 동작하지만, 비즈니스 로직은 여러 서비스의 협업으로 실행됩니다.
주문 서비스가 재고 확인과 결제를 포함한다면 다음과 같은 문제가 발생할 수 있습니다.
1. 고객이 주문
2. 주문 서비스 → 재고 서비스: 재고 차감 요청 ✅ 성공
3. 주문 서비스 → 결제 서비스: 결제 요청 ❌ 실패 (서버 오류 / 카드 오류 등)
이 상황에서 두 가지 핵심 문제가 발생합니다.
재고 서비스는 이미 재고를 차감했는데, 결제는 실패한 상태.
→ 고객은 결제를 하지 않았는데 재고가 줄어드는 모순된 상태 발생
원자성이란, 트랜잭션이 모두 성공하거나 모두 실패해야 한다는 원칙입니다.
| 단일 DB 트랜잭션 | 분산 시스템 |
|---|---|
| 재고 차감 + 결제 → 둘 다 성공 or 둘 다 롤백 | 서비스가 독립 실행 → 한쪽만 성공 가능 |
💡 쉽게 기억하는 방법
- 데이터 불일치: 한쪽만 처리되고 다른 쪽은 실패
- 원자성 문제: 모든 것이 한꺼번에 성공하거나 실패해야 하는데 그렇지 않음
➡️ 이를 해결하기 위해 분산 트랜잭션이 필요하며, Saga 패턴은 대표적인 해결책 중 하나입니다.
Saga 패턴은 분산 트랜잭션을 일련의 로컬 트랜잭션으로 분해하여 해결하는 방법입니다.
오케스트레이터(Orchestrator) 서비스가 전체 흐름을 중앙에서 관리하며, 각 서비스에 실행 또는 보상 트랜잭션을 지시합니다.
오케스트레이터
├── [1] 재고 서비스 → 재고 차감 요청
│ ✅ 성공 → 다음 단계 진행
├── [2] 결제 서비스 → 결제 요청
│ ❌ 실패 → 보상 트랜잭션 실행
└── [보상] 재고 서비스 → 재고 복구
class Orchestrator:
def process_order(self, order):
try:
inventory_service.decrease_stock(order.product_id)
payment_service.process_payment(order.payment_info)
delivery_service.schedule_delivery(order)
except Exception as e:
self.rollback(order)
def rollback(self, order):
delivery_service.cancel_delivery(order)
payment_service.refund_payment(order.payment_info)
inventory_service.increase_stock(order.product_id)
| 장점 | 단점 |
|---|---|
| 전체 흐름이 명확하게 파악됨 | 복잡한 비즈니스 로직이 오케스트레이터에 집중 |
| 중앙 관리로 모니터링 용이 | 오케스트레이터 자체가 단일 장애 지점이 될 수 있음 |
No! 오케스트레이터는 별도의 독립 서버로 운영해야 합니다.
| 구분 | 역할 |
|---|---|
| 공용 서버 | API 게이트웨이, 인증 서버 등 여러 서비스가 공용으로 접근하는 리소스 역할 |
| 오케스트레이터 서버 | 오직 분산 트랜잭션 흐름 관리만 담당 (주문 → 재고 → 결제 → 배송 순서 관리) |
역할이 완전히 다르므로 분리 운영해야 유지보수와 모니터링이 쉬워집니다.
중앙 관리자 없이, 각 서비스가 이벤트를 보고 다음 행동을 알아서 이어가는 방식입니다.
주문 서비스 → 재고 차감 요청
재고 서비스 → 성공 후 결제 서비스 호출
결제 서비스 → 실패 시 재고 서비스에 보상 트랜잭션 호출
| 장점 | 단점 |
|---|---|
| 오케스트레이터 서버 불필요 | 서비스 간 의존성이 강해질 수 있음 |
| 소규모 시스템에 적합 | 전체 흐름 파악이 어려움 |
No! 개념과 도구를 구분해야 합니다.
| 구분 | 설명 |
|---|---|
| 체이닝 Saga | 이벤트 기반으로 서비스들이 이어지는 방식 (개념/패턴) |
| Kafka, RabbitMQ | 이벤트를 전달해주는 도구 (기술) |
체이닝 Saga를 구현할 때 Kafka를 사용할 수 있지만, 둘은 동일한 개념이 아닙니다.
모든 트랜잭션 실행 전 이전 상태를 로그에 기록합니다. 실패 시 로그를 참조해 보상 트랜잭션을 실행합니다.
def decrease_stock(product_id):
log_before_state(product_id) # 이전 상태 기록
try:
# 재고 차감 로직
pass
except Exception:
rollback_to_previous_state(product_id) # 로그 기반 롤백
트랜잭션 상태를 별도 테이블에 기록하여 상태 기반 보상을 수행합니다.
| Transaction ID | Service | State |
|---|---|---|
| 12345 | Inventory | Completed |
| 12345 | Payment | Failed |
보상 트랜잭션 실행 시 테이블을 참조하여 실패한 작업에만 롤백을 수행합니다.
맞지만, 상황에 따라 다릅니다.
각 서비스(주문, 재고, 결제)가 서로 다른 DB를 가지고 있기 때문에, 하나의 흐름을 묶기 위한 공용 ID가 필요합니다.
transactionId = 12345
주문 서비스 → txId: 12345
재고 서비스 → txId: 12345
결제 서비스 → txId: 12345
→ "이 트랜잭션이 어디까지 처리됐지?" 를 추적할 수 있게 됩니다.
12345 | Inventory | SUCCESS
12345 | Payment | FAILED
| 방식 | 상태 관리 방법 |
|---|---|
| Orchestration | Saga 상태 테이블에서 모든 단계 상태를 중앙 관리 |
| Choreography | 각 서비스가 자기 상태만 개별 관리 |
| Kafka 기반 | DB 대신 Kafka 이벤트 로그를 사실상의 상태 기록으로 활용 |
| 기술 | 역할 |
|---|---|
| Spring Boot + Kafka | Kafka 이벤트 스트리밍으로 트랜잭션 상태 비동기 관리 |
| Axon Framework | 오케스트레이션 및 이벤트 기반 Saga 패턴 지원 프레임워크 |
| gRPC / REST | 서비스 간 통신 방식 |
| Transaction Logs | 보상 트랜잭션 실행을 위한 이중 로그 시스템 |
| State Machine | 트랜잭션 상태 시각화 및 관리 |
분산 시스템의 문제
├── 데이터 불일치 → 한쪽만 처리되고 다른 쪽 실패
└── 원자성 문제 → 전부 성공 or 전부 실패가 보장되지 않음
Saga 패턴으로 해결
├── 오케스트레이션 → 중앙 오케스트레이터가 전체 흐름 관리
└── 코레오그래피 → 각 서비스가 이벤트 기반으로 자율 처리
보상 트랜잭션으로 롤백
├── 이중 로그 방식 → 이전 상태 기록 후 실패 시 복원
└── 상태 테이블 → UUID 기반 트랜잭션 상태 추적