분산 시스템의 데이터 일관성 문제와 Saga 패턴

한소연·2026년 4월 10일

내일배움캠프

목록 보기
15/21
post-thumbnail

분산 시스템에서는 하나의 서비스가 독립적으로 동작하지만, 비즈니스 로직은 여러 서비스의 협업으로 실행됩니다.


1. 왜 분산 트랜잭션이 필요한가?

주문 서비스가 재고 확인결제를 포함한다면 다음과 같은 문제가 발생할 수 있습니다.

문제 시나리오

1. 고객이 주문
2. 주문 서비스 → 재고 서비스: 재고 차감 요청 ✅ 성공
3. 주문 서비스 → 결제 서비스: 결제 요청 ❌ 실패 (서버 오류 / 카드 오류 등)

이 상황에서 두 가지 핵심 문제가 발생합니다.

🔴 데이터 불일치

재고 서비스는 이미 재고를 차감했는데, 결제는 실패한 상태.
→ 고객은 결제를 하지 않았는데 재고가 줄어드는 모순된 상태 발생

🔴 원자성 문제

원자성이란, 트랜잭션이 모두 성공하거나 모두 실패해야 한다는 원칙입니다.

단일 DB 트랜잭션분산 시스템
재고 차감 + 결제 → 둘 다 성공 or 둘 다 롤백서비스가 독립 실행 → 한쪽만 성공 가능

💡 쉽게 기억하는 방법

  • 데이터 불일치: 한쪽만 처리되고 다른 쪽은 실패
  • 원자성 문제: 모든 것이 한꺼번에 성공하거나 실패해야 하는데 그렇지 않음

➡️ 이를 해결하기 위해 분산 트랜잭션이 필요하며, Saga 패턴은 대표적인 해결책 중 하나입니다.


2. Saga 패턴이란?

Saga 패턴은 분산 트랜잭션을 일련의 로컬 트랜잭션으로 분해하여 해결하는 방법입니다.

  • 로컬 트랜잭션: 각 서비스에서 독립적으로 실행되는 트랜잭션
  • 보상 트랜잭션 (Compensating Transaction): 실패 시 이전 단계의 작업을 취소하거나 원래 상태로 되돌리는 작업

3. Saga 패턴의 두 가지 구현 방식

3-1. 오케스트레이션 기반 Saga (Orchestration)

오케스트레이터(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 게이트웨이, 인증 서버 등 여러 서비스가 공용으로 접근하는 리소스 역할
오케스트레이터 서버오직 분산 트랜잭션 흐름 관리만 담당 (주문 → 재고 → 결제 → 배송 순서 관리)

역할이 완전히 다르므로 분리 운영해야 유지보수와 모니터링이 쉬워집니다.


3-2. 체이닝 기반 Saga (Choreography)

중앙 관리자 없이, 각 서비스가 이벤트를 보고 다음 행동을 알아서 이어가는 방식입니다.

흐름

주문 서비스 → 재고 차감 요청
재고 서비스 → 성공 후 결제 서비스 호출
결제 서비스 → 실패 시 재고 서비스에 보상 트랜잭션 호출

특징

장점단점
오케스트레이터 서버 불필요서비스 간 의존성이 강해질 수 있음
소규모 시스템에 적합전체 흐름 파악이 어려움

❓ 체이닝 기반 Saga는 Kafka나 RabbitMQ 같은 메시지 큐인가?

No! 개념과 도구를 구분해야 합니다.

구분설명
체이닝 Saga이벤트 기반으로 서비스들이 이어지는 방식 (개념/패턴)
Kafka, RabbitMQ이벤트를 전달해주는 도구 (기술)

체이닝 Saga를 구현할 때 Kafka를 사용할 수 있지만, 둘은 동일한 개념이 아닙니다.


4. 보상 트랜잭션 관리 전략

전략 1: 이중 로그 (Compensating Logs)

모든 트랜잭션 실행 전 이전 상태를 로그에 기록합니다. 실패 시 로그를 참조해 보상 트랜잭션을 실행합니다.

def decrease_stock(product_id):
    log_before_state(product_id)  # 이전 상태 기록
    try:
        # 재고 차감 로직
        pass
    except Exception:
        rollback_to_previous_state(product_id)  # 로그 기반 롤백

전략 2: 상태 테이블 활용

트랜잭션 상태를 별도 테이블에 기록하여 상태 기반 보상을 수행합니다.

Transaction IDServiceState
12345InventoryCompleted
12345PaymentFailed

보상 트랜잭션 실행 시 테이블을 참조하여 실패한 작업에만 롤백을 수행합니다.


5. 트랜잭션 상태 관리와 UUID

❓ 트랜잭션을 UUID로 DB에 저장하여 관리하는 건가?

맞지만, 상황에 따라 다릅니다.

UUID를 쓰는 이유

각 서비스(주문, 재고, 결제)가 서로 다른 DB를 가지고 있기 때문에, 하나의 흐름을 묶기 위한 공용 ID가 필요합니다.

transactionId = 12345

주문 서비스  → txId: 12345
재고 서비스  → txId: 12345
결제 서비스  → txId: 12345

→ "이 트랜잭션이 어디까지 처리됐지?" 를 추적할 수 있게 됩니다.

성공/실패를 DB에 저장하는 이유

12345 | Inventory | SUCCESS
12345 | Payment   | FAILED
  1. 상태 추적: 어느 단계에서 실패했는지 즉시 파악 가능
  2. 보상 트랜잭션: "결제 실패 → 재고 복구" 판단을 위해 현재 상태 확인 필요
  3. 장애 복구: 서버 재시작 후에도 DB를 보고 이어서 처리 가능

방식별 상태 관리 비교

방식상태 관리 방법
OrchestrationSaga 상태 테이블에서 모든 단계 상태를 중앙 관리
Choreography각 서비스가 자기 상태만 개별 관리
Kafka 기반DB 대신 Kafka 이벤트 로그를 사실상의 상태 기록으로 활용

6. 도구와 기술 스택

기술역할
Spring Boot + KafkaKafka 이벤트 스트리밍으로 트랜잭션 상태 비동기 관리
Axon Framework오케스트레이션 및 이벤트 기반 Saga 패턴 지원 프레임워크
gRPC / REST서비스 간 통신 방식
Transaction Logs보상 트랜잭션 실행을 위한 이중 로그 시스템
State Machine트랜잭션 상태 시각화 및 관리

정리

분산 시스템의 문제
├── 데이터 불일치  →  한쪽만 처리되고 다른 쪽 실패
└── 원자성 문제    →  전부 성공 or 전부 실패가 보장되지 않음

Saga 패턴으로 해결
├── 오케스트레이션  →  중앙 오케스트레이터가 전체 흐름 관리
└── 코레오그래피    →  각 서비스가 이벤트 기반으로 자율 처리

보상 트랜잭션으로 롤백
├── 이중 로그 방식  →  이전 상태 기록 후 실패 시 복원
└── 상태 테이블     →  UUID 기반 트랜잭션 상태 추적
profile
안 되면 될 때까지

0개의 댓글