[Daily morning study] Saga 패턴과 분산 트랜잭션

#daily morning study

Image


분산 트랜잭션 문제

마이크로서비스 아키텍처에서 각 서비스는 자체 데이터베이스를 소유한다. 단일 비즈니스 트랜잭션이 여러 서비스에 걸쳐 있을 때 데이터 일관성을 유지하기가 어려워진다.

예를 들어 주문 처리 흐름:

  1. 주문 서비스 → 주문 생성
  2. 재고 서비스 → 재고 차감
  3. 결제 서비스 → 결제 처리
  4. 배송 서비스 → 배송 요청

이 중 하나가 실패하면? 모놀리식에서는 DB 트랜잭션으로 롤백하면 됐지만, MSA에서는 각 서비스가 이미 자체 DB에 커밋한 상태라 단순 롤백이 불가능하다.

2PC(Two-Phase Commit)의 한계

전통적인 분산 트랜잭션 해결책인 2PC는 두 단계로 동작한다.

  • Prepare 단계: 코디네이터가 모든 참여자에게 “커밋 준비됐어?” 확인
  • Commit 단계: 모두 OK면 커밋, 하나라도 NO면 롤백

하지만 MSA 환경에서는 여러 문제가 있다.

  • 코디네이터가 SPOF(단일 장애 지점)
  • 블로킹 프로토콜 → 락을 오래 보유해 성능 저하
  • 네트워크 파티션 발생 시 불일치 가능
  • 서비스 자율성을 해침

Saga 패턴

Saga는 분산 환경에서 데이터 일관성을 보장하는 패턴이다.

핵심 아이디어: 긴 트랜잭션을 여러 개의 로컬 트랜잭션으로 분해하고, 각 단계가 실패하면 이전에 성공한 트랜잭션을 취소하는 보상 트랜잭션(Compensating Transaction)을 실행한다.

완전한 롤백 대신 “이미 일어난 일을 의미적으로 되돌리는” 방식이다.

Saga 구현 방식

1. Choreography (코레오그래피)

각 서비스가 로컬 트랜잭션 완료 후 이벤트를 발행하고, 다음 서비스가 그 이벤트를 구독해서 처리한다. 중앙 오케스트레이터 없이 서비스끼리 이벤트로만 소통한다.

성공 흐름:

주문서비스  → OrderPlaced 이벤트 발행
재고서비스  → 구독 → 재고 차감 → StockReserved 이벤트 발행
결제서비스  → 구독 → 결제 처리 → PaymentCompleted 이벤트 발행
배송서비스  → 구독 → 배송 요청 → ShippingRequested 이벤트 발행

결제 실패 시 보상 흐름:

결제서비스  → PaymentFailed 이벤트 발행
재고서비스  → 구독 → 재고 복구 → StockReleased 이벤트 발행
주문서비스  → 구독 → 주문 취소

장점

  • 서비스 간 직접 결합이 없음 (느슨한 결합)
  • 중앙 코디네이터 없음 → 단일 장애 지점 없음

단점

  • 전체 흐름을 한눈에 파악하기 어려움
  • 서비스 간 순환 이벤트 의존이 생길 수 있음
  • 디버깅과 추적이 복잡

2. Orchestration (오케스트레이션)

중앙 Saga Orchestrator가 각 서비스에 명령을 내리고 결과를 받아 다음 단계를 결정한다.

SagaOrchestrator
  ├─ OrderService.createOrder()         → 성공
  ├─ InventoryService.reserveStock()    → 성공
  ├─ PaymentService.processPayment()    → 실패
  ├─ InventoryService.releaseStock()    (보상 트랜잭션)
  └─ OrderService.cancelOrder()         (보상 트랜잭션)
class OrderSagaOrchestrator {
    void execute(OrderCommand cmd) {
        String orderId = orderService.createOrder(cmd);
        try {
            inventoryService.reserveStock(cmd.getItemId(), cmd.getQty());
            try {
                paymentService.processPayment(cmd.getPaymentInfo());
                shippingService.requestShipping(cmd.getAddress());
            } catch (PaymentFailedException e) {
                inventoryService.releaseStock(cmd.getItemId(), cmd.getQty());
                orderService.cancelOrder(orderId);
            }
        } catch (StockUnavailableException e) {
            orderService.cancelOrder(orderId);
        }
    }
}

장점

  • 전체 흐름을 오케스트레이터 하나에서 명확하게 파악 가능
  • 복잡한 비즈니스 로직 관리 용이
  • 개별 서비스는 단순하게 유지

단점

  • 오케스트레이터가 복잡해질 수 있음
  • 오케스트레이터와 각 서비스 간 결합도가 높아짐

보상 트랜잭션(Compensating Transaction)

Saga에서 실패 시 이전에 성공한 로컬 트랜잭션을 취소하는 작업이다.

중요한 특징:

  • 물리적 롤백이 아니라 의미적(semantic) 롤백
  • 이미 커밋된 데이터를 되돌리는 새로운 비즈니스 로직
  • 모든 로컬 트랜잭션에 대한 보상 트랜잭션을 미리 설계해야 함
로컬 트랜잭션보상 트랜잭션
주문 생성주문 취소
재고 차감재고 복구
결제 처리환불 처리
배송 요청배송 취소

멱등성(Idempotency)

네트워크 재시도나 메시지 큐의 중복 전달로 인해 같은 이벤트가 여러 번 처리될 수 있다. 동일한 요청이 여러 번 들어와도 결과가 같아야 한다.

void reserveStock(String sagaId, String itemId, int qty) {
    if (reservationRepository.existsBySagaId(sagaId)) {
        return; // 이미 처리된 요청 → 무시
    }
    stock.decrease(itemId, qty);
    reservationRepository.save(sagaId, itemId, qty);
}

sagaId를 유니크 키로 저장해 중복 처리를 방지한다.

Saga vs 2PC 비교

항목2PCSaga
일관성 수준강한 일관성(ACID)최종 일관성(Eventual Consistency)
성능낮음 (락 보유)높음
가용성코디네이터 장애 시 블로킹고가용성
구현 복잡도낮음 (DB 수준 지원)높음 (보상 트랜잭션 설계 필요)
MSA 적합성낮음높음

격리성 부재 문제

Saga는 ACID 중 격리성(Isolation) 이 없다. 각 로컬 트랜잭션이 커밋되는 순간 다른 Saga가 중간 상태의 데이터를 읽을 수 있다.

이를 해결하기 위한 전략:

  • Semantic Lock: 처리 중인 레코드에 플래그를 설정해 다른 트랜잭션이 접근하지 못하게 함
  • Commutative Update: 순서에 관계없이 결과가 같은 업데이트 설계
  • Pessimistic View: 최악의 상태를 가정하고 UI/비즈니스 로직 설계

실제 구현 도구

  • Eventuate Tram Saga: Spring 기반 Saga 프레임워크, Choreography/Orchestration 모두 지원
  • AWS Step Functions: 오케스트레이션 기반 분산 워크플로우 서비스
  • Temporal.io: 내구성 있는 워크플로우 실행 플랫폼
  • Apache Camel: Saga EIP(Enterprise Integration Pattern) 지원

핵심 정리

  • 분산 환경에서 2PC 대신 Saga로 데이터 일관성 확보
  • 로컬 트랜잭션 + 보상 트랜잭션 쌍으로 설계
  • 코레오그래피(이벤트 기반, 느슨한 결합) vs 오케스트레이션(중앙 제어, 명확한 흐름)
  • 멱등성 처리 필수
  • 격리성 없음 → 최종 일관성(Eventual Consistency) 모델 수용