본문으로 건너뛰기
김신건의 로그

[Pattern] Saga: 분산 트랜잭션의 보상 흐름

· 수정 · 📖 약 3분 · 1,047자/단어 #saga #distributed-transaction #microservices #backend
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 안)
  • 문제 추적 쉬움.
항목ChoreographyOrchestration
결합도낮음약간 높음
흐름 추적어려움쉬움
디버깅어려움쉬움
적합단순 흐름 (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 topicevent log 자연 통합
AWS Step Functionsmanaged orchestrator
Temporal워크플로 엔진
CamundaBPMN 표준

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

  1. 모든 분산 흐름 = saga = 단순 async event 가 충분한데도 복잡한 saga. 진짜 보상 필요한 흐름에만.
  2. 보상 없거나 미구현 = “해피 패스만 만들고 실패는 운영자 처리”. 1년 뒤 데이터 불일치 폭증.
  3. Choreography 의 순환 의존성 = A → B → C → A 같은 흐름. 디버깅 지옥.
  4. 상태 저장 안 함 = orchestrator 다운 시 진행 잃음. event log 또는 DB 필수.

Saga 상태 기계

stateDiagram-v2
    [*] --> STARTED
    STARTED --> PROCESSING : 단계 실행
    PROCESSING --> PROCESSING : 다음 단계
    PROCESSING --> COMPENSATING : 단계 실패
    COMPENSATING --> COMPENSATING : 보상 단계 실행
    COMPENSATING --> FAILED : 보상 완료
    PROCESSING --> COMPLETED : 모든 단계 성공
    FAILED --> [*]
    COMPLETED --> [*]
상태의미
STARTEDsaga 생성, 첫 단계 전
PROCESSING정방향 단계 실행 중
COMPENSATING실패 후 보상 단계 실행 중
COMPLETED모든 단계 성공
FAILED보상 완료, 논리적 롤백 완료

Saga 와 2PC 비교

항목Saga2PC (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 로 분산 추적 연결
  • 보상 단계 실패 시 수동 개입 알람

관련 위키

이 글의 용어 (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 | 시나리오 |…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기