[Pattern] Saga: 분산 트랜잭션의 보상 흐름
Saga pattern, Choreography saga, Orchestration saga, compensating transaction, long-running transaction
정의
Saga = 여러 service 의 로컬 트랜잭션 을 연결한 긴 흐름. 한 단계 실패 시 이전 단계의 보상 (compensating) 트랜잭션 으로 논리적 롤백.
IMPORTANT
Saga 는 ACID 트랜잭션이 아니다. 분산 환경에서 2PC (XA) 가 비현실적이라 eventually consistent + 보상 으로 대체.
시나리오: 주문
sequenceDiagram
autonumber
participant O as Order Service
participant P as Payment Service
participant I as Inventory Service
participant S as Shipping Service
O->>O: T1: 주문 생성 (pending)
O->>P: T2: 결제 요청
P-->>O: 결제 완료
O->>I: T3: 재고 차감
I-->>O: 차감 완료
O->>S: T4: 배송 요청
S-->>O: 배송 시작
O->>O: T5: 주문 confirmed
실패 시 보상:
sequenceDiagram
O->>P: T2 결제
P-->>O: ✓
O->>I: T3 재고
I-->>O: 실패 (재고 부족)
O->>P: C2: 결제 환불 (보상)
O->>O: C1: 주문 취소 (보상)
| 단계 | Compensating |
|---|---|
| T1 주문 생성 | C1 주문 취소 |
| T2 결제 | C2 환불 |
| T3 재고 차감 | C3 재고 복원 |
| T4 배송 | C4 배송 취소 |
두 스타일: Choreography vs Orchestration
1. Choreography (이벤트 기반)
flowchart LR
O[Order] -->|OrderCreated| P[Payment]
P -->|PaymentSucceeded| I[Inventory]
I -->|StockReserved| S[Shipping]
S -->|Shipped| O
P -->|PaymentFailed| O
I -->|StockUnavailable| P
- 각 service 가 event 듣고 자기 일.
- 중앙 제어자 없음.
- 유연, 낮은 결합.
- 흐름 추적 어려움 (어디까지 갔는지).
2. Orchestration (중앙 코디네이터)
flowchart LR
Coord[Saga Orchestrator] --> O[Order]
O -->|성공| Coord
Coord --> P[Payment]
P -->|성공| Coord
Coord --> I[Inventory]
I -->|실패| Coord
Coord -->|보상| P
Coord -->|보상| O
- 중앙 상태 기계.
- 흐름 명확.
- 결합도 약간 높음 (orchestrator 가 모든 service 안)
- 문제 추적 쉬움.
| 항목 | Choreography | Orchestration |
|---|---|---|
| 결합도 | 낮음 | 약간 높음 |
| 흐름 추적 | 어려움 | 쉬움 |
| 디버깅 | 어려움 | 쉬움 |
| 적합 | 단순 흐름 (3-4 단계) | 복잡 흐름 (5+ 단계) |
| 운영 도구 | event tracer | 상태 기계 시각화 |
TIP
3-4 단계까지는 choreography, 그 이상은 orchestration 이 일반 권장. Netflix 도 Conductor 라는 orchestrator 사용.
보상 트랜잭션의 함정
flowchart TD
Q[보상이 *항상 가능* 한가?]
Q --> X1["✗ 이메일 발송 (취소 불가)"]
Q --> X2["✗ SMS / push 알림"]
Q --> X3["✗ 외부 API 호출 (paypal 환불 한도)"]
Q --> Sol1["→ 가능한 단계만 saga 안"]
Q --> Sol2["→ 비가역 단계는 saga *마지막* 으로"]
CAUTION
비가역 작업 (이메일, push, 외부 API) 은 saga 가 commit 결정 후 실행. 그 전에 실행하면 보상 불가.
멱등성 + Idempotency Key
각 단계 / 보상은 멱등 (idempotent) 해야 한다. 자세한 건 idempotency-keys.
# 보상 시
async def compensate_payment(saga_id, payment_id):
if already_refunded(saga_id):
return # 멱등
await payment_service.refund(payment_id, key=saga_id)
Saga 상태 저장
flowchart LR
Orch[Orchestrator] --> Store[(Saga State Store)]
Store --> SQL[(PostgreSQL)]
Store --> Stream[(Kafka)]
Store --> SF[(Step Functions)]
| Store | 특징 |
|---|---|
| PostgreSQL (DB) | 트랜잭션 친화 |
| Kafka topic | event log 자연 통합 |
| AWS Step Functions | managed orchestrator |
| Temporal | 워크플로 엔진 |
| Camunda | BPMN 표준 |
Temporal / Step Functions (워크플로 엔진)
flowchart LR
SDK["Application code<br/>(workflow + activity)"] --> Temporal[Temporal Server]
Temporal --> StateDB[(상태 DB)]
Temporal --> Workers["Workers<br/>(activity 실행)"]
- 워크플로 코드 가 durable execution.
- retry / timeout / sleep 이 언어 native 처럼.
- 마치 함수 호출 같은데 분산 + 영속.
흔한 함정
WARNING
- 모든 분산 흐름 = saga = 단순 async event 가 충분한데도 복잡한 saga. 진짜 보상 필요한 흐름에만.
- 보상 없거나 미구현 = “해피 패스만 만들고 실패는 운영자 처리”. 1년 뒤 데이터 불일치 폭증.
- Choreography 의 순환 의존성 = A → B → C → A 같은 흐름. 디버깅 지옥.
- 상태 저장 안 함 = orchestrator 다운 시 진행 잃음. event log 또는 DB 필수.
Saga 상태 기계
stateDiagram-v2
[*] --> STARTED
STARTED --> PROCESSING : 단계 실행
PROCESSING --> PROCESSING : 다음 단계
PROCESSING --> COMPENSATING : 단계 실패
COMPENSATING --> COMPENSATING : 보상 단계 실행
COMPENSATING --> FAILED : 보상 완료
PROCESSING --> COMPLETED : 모든 단계 성공
FAILED --> [*]
COMPLETED --> [*]
| 상태 | 의미 |
|---|---|
| STARTED | saga 생성, 첫 단계 전 |
| PROCESSING | 정방향 단계 실행 중 |
| COMPENSATING | 실패 후 보상 단계 실행 중 |
| COMPLETED | 모든 단계 성공 |
| FAILED | 보상 완료, 논리적 롤백 완료 |
Saga 와 2PC 비교
| 항목 | Saga | 2PC (XA) |
|---|---|---|
| 트랜잭션 격리 | 약함 (eventually consistent) | 강함 (ACID) |
| 가용성 | 높음 (부분 실패 허용) | 낮음 (코디네이터 블로킹) |
| 구현 복잡도 | 보상 로직 필요 | 분산 코디네이터 필요 |
| 성능 | 비동기 가능 | 동기, 잠금 |
| 적합 | 마이크로서비스 | 단일 조직 DB 간 |
NOTE
2PC 가 필요하다고 느껴진다면 saga 를 먼저 검토. saga 로 해결 안 되는 경우에만 2PC.
Temporal 워크플로 예시
# Temporal Python SDK
from temporalio import workflow, activity
from datetime import timedelta
@workflow.defn
class OrderSaga:
@workflow.run
async def run(self, order_id: str) -> str:
# T1: 주문 확인
order = await workflow.execute_activity(
confirm_order,
order_id,
start_to_close_timeout=timedelta(seconds=10),
)
try:
# T2: 결제
payment = await workflow.execute_activity(
charge_payment,
order.payment_info,
start_to_close_timeout=timedelta(seconds=30),
)
except Exception:
# C1: 보상 - 주문 취소
await workflow.execute_activity(cancel_order, order_id)
raise
try:
# T3: 재고 차감
await workflow.execute_activity(
deduct_stock,
order.items,
start_to_close_timeout=timedelta(seconds=10),
)
except Exception:
# C2: 결제 환불
await workflow.execute_activity(refund_payment, payment.id)
await workflow.execute_activity(cancel_order, order_id)
raise
return "completed"
Temporal 의 핵심:
- activity 실패 시 자동 retry (정책 설정 가능)
- workflow state 는 영속 저장: 서버 재시작 후에도 재개
- 언어 native 코드처럼 쓰지만 분산 + 내구성
Saga 테스트 전략
| 테스트 레벨 | 목적 | 도구 |
|---|---|---|
| Unit | 각 서비스 보상 로직 멱등성 | Jest / pytest |
| Integration | 오케스트레이터 + 스텁 서비스 | Testcontainers |
| Contract | 서비스 간 이벤트 스키마 | Pact |
| End-to-End | 실패 시나리오 전체 흐름 | Temporal test env |
# 멱등성 단위 테스트 예시
def test_compensate_payment_idempotent():
saga_id = "test-saga-1"
payment_id = "pay-1"
# 첫 번째 보상
compensate_payment(saga_id, payment_id)
# 두 번째 보상 (중복) 도 에러 없이 통과
compensate_payment(saga_id, payment_id)
# DB 에 환불 기록 1건만
assert count_refunds(payment_id) == 1
Saga 테스트 핵심: 실패 경로마다 보상이 올바르게 실행되는지, 보상 자체가 실패할 때 어떻게 되는지 검증.
프로덕션 체크리스트
- 각 단계에 Idempotency Key 부여 (재시도 안전)
- 모든 보상 트랜잭션 구현 및 멱등성 검증
- Saga 상태 영속 저장 (DB 또는 워크플로 엔진)
- 비가역 작업 (이메일, push) saga 마지막에 배치
- 실패 알람 및 Dead Letter Queue 운영
- 각 단계 timeout + retry 정책 설정
- correlationId / traceid 로 분산 추적 연결
- 보상 단계 실패 시 수동 개입 알람
관련 위키
- distributed-systems-distributed-transaction (2PC)
- outbox-pattern (event publishing 보장)
- idempotency-keys
- event-sourcing
- microservices-vs-monolith
- cqrs
- cap-theorem
이 글의 용어 (7개)
- [Distributed Systems] 분산 트랜잭션: 2PC, Saga, Outboxdistributed-systems
- 정의 분산 트랜잭션 은 여러 service / DB / 메시지 큐에 걸친 작업을 ACID 처럼 묶는 문제. 마이크로서비스 / 이벤트 기반 아키텍처의 핵심 도전. 3가지 접근: 1…
- [Distributed Systems] CAP Theorem과 PACELCdistributed-systems
- 정의 CAP Theorem (Eric Brewer, 2000): 분산 시스템에서 3가지 중 2개만 동시에 보장 가능. - C (Consistency): 모든 노드가 같은 시점에 …
- [Pattern] CQRS: Command / Query 분리, read modeldistributed-systems
- 정의 CQRS (Command Query Responsibility Segregation) = 쓰기 모델 과 읽기 모델 을 분리. 같은 데이터를 다른 형태 로 저장 / 조회. […
- [Pattern] Event Sourcing: 상태 대신 이벤트 누적distributed-systems
- 정의 Event Sourcing = 현재 상태 대신 과거 이벤트 시퀀스 를 저장. 현재 상태 = event 모두 replay 한 결과. 핵심 약속 - 과거 모든 변화 기록. 감사…
- [Pattern] Idempotency Keys: 중복 요청 안전 처리distributed-systems
- 정의 Idempotency = 같은 요청을 N번 보내도 결과가 1번과 동일. 분산 시스템 / 결제 / API 의 안전망. [!IMPORTANT] 네트워크는 항상 timeout /…
- [Pattern] Microservices vs Monolith: 언제 분리, 언제 통합distributed-systems
- 정의 | - | Monolith | Modular Monolith | Microservices | |---|---|---|---| | 배포 단위 | 1개 | 1개 (모듈 명확) …
- [Pattern] Outbox Pattern: DB + 메시지의 원자성distributed-systems
- 정의 Outbox Pattern = DB 변경 + 메시지 발행 의 원자성 보장. 이중 쓰기 (dual write) 문제 의 표준 해결. 문제: Dual Write | 시나리오 |…
💬 댓글