분산 마이크로서비스 환경에서 데이터 정합성을 보장하는 사가(Saga) 패턴과 보상 트랜잭션 설계

분산 마이크로서비스 환경에서 데이터 정합성을 보장하는 사가(Saga) 패턴과 보상 트랜잭션 설계
분산 마이크로서비스 환경에서 데이터 정합성을 보장하는 사가(Saga) 패턴과 보상 트랜잭션 설계

모놀리식 아키텍처가 독립된 분산 데이터베이스를 가진 마이크로서비스로 분할되면서 엔지니어가 마주하는 가장 까다로운 난제는 단일 물리 트랜잭션(ACID)의 상실과 분산 데이터 정합성 유지입니다.

전통적인 분산 트랜잭션 프로토콜인 2단계 커밋(2PC, Two-Phase Commit)이나 XA 트랜잭션은 모든 참여 노드가 락(Lock)을 획득하고 응답할 때까지 대기해야 하므로, 네트워크 지연(Latency)과 단일 장애점(SPOF)으로 인해 고가용성 현대 클라우드 시스템에는 적합하지 않습니다. 본 글에서는 로컬 트랜잭션의 연쇄적 실행과 실패 시 역방향 보상 트랜잭션(Compensating Transaction)을 통해 최종 일관성(Eventual Consistency)을 보장하는 사가(Saga) 패턴의 오케스트레이션 아키텍처를 분석합니다.

1. 코레오그래피(Choreography) vs 오케스트레이션(Orchestration) 비교

사가 패턴은 서비스 간 상호작용 제어 방식에 따라 크게 두 가지 방식으로 구현됩니다.

  • 코레오그래피(Choreography): 중앙 조율자 없이 각 서비스가 도메인 이벤트를 발행하고 구독하여 다음 단계를 실행합니다. 구조가 단순하지만 워크플로가 복잡해지면 이벤트 흐름을 추적하기 어렵고 순환 의존성(Cyclic Dependency)이 발생할 위험이 있습니다.
  • 오케스트레이션(Orchestration): 전용 사가 오케스트레이터(Orchestrator)가 상태 머신을 기반으로 참여 서비스들에게 실행할 커맨드를 전달하고 응답을 수집합니다. 전체 비즈니스 흐름을 한곳에서 모니터링할 수 있고 보상 트랜잭션 제어가 명확하여 복잡한 결제/주문 파이프라인의 엔터프라이즈 표준으로 권장됩니다.

2. 사가 오케스트레이터와 보상 트랜잭션 상태 머신 구현

오케스트레이터는 각 단계(Step)마다 순방향 액션(Action)과 롤백을 위한 역방향 보상 액션(Compensate)을 쌍으로 관리합니다. 중간 단계에서 오류가 발생하면 이전까지 성공했던 모든 단계의 보상 트랜잭션을 역순으로 호출합니다.

# 사가 오케스트레이터 및 역방향 보상 트랜잭션 실행 엔진 개념 구현체
import logging
from typing import List, Callable, Any

logger = logging.getLogger("SagaOrchestrator")

class SagaStep:
    def __init__(self, name: str, action: Callable[[Any], bool], compensate: Callable[[Any], bool]):
        self.name = name
        self.action = action          # 순방향 실행 로직
        self.compensate = compensate  # 실패 시 역방향 보상 로직

class OrderSagaOrchestrator:
    def __init__(self, steps: List[SagaStep]):
        self.steps = steps
        self.executed_steps: List[SagaStep] = []

    def execute(self, payload: dict) -> bool:
        logger.info("사가 워크플로 가동: order_id=%s", payload.get("order_id"))
        
        for step in self.steps:
            try:
                logger.info("[진행] %s 액션 실행 중...", step.name)
                success = step.action(payload)
                if not success:
                    raise RuntimeError(f"{step.name} 비즈니스 검증 실패")
                self.executed_steps.append(step)
            except Exception as exc:
                logger.error("[오류 발생] %s 단계 실패: %s. 보상 트랜잭션을 가동합니다.", step.name, exc)
                self.rollback(payload)
                return False
        
        logger.info("사가 워크플로 100% 정상 완료: order_id=%s", payload.get("order_id"))
        return True

    def rollback(self, payload: dict):
        """실행 완료된 스텝들을 역순으로 순회하며 보상 트랜잭션 수행"""
        for step in reversed(self.executed_steps):
            try:
                logger.warning("[보상 롤백] %s 보상 액션 실행 중...", step.name)
                step.compensate(payload)
            except Exception as comp_exc:
                # 보상 트랜잭션 실패 시 DLQ 또는 운영자 수동 개입 알림 전송
                logger.critical("[심각] %s 보상 액션 실패! 수동 정합성 복구 필요: %s", step.name, comp_exc)

위 코드와 같이 OrderSagaOrchestrator는 ‘주문 생성 -> 결제 승인 -> 재고 차감 -> 배송 접수’ 흐름 중 ‘재고 차감’이 실패하면, 이미 성공했던 ‘결제 승인’을 취소하는 보상 액션(결제 환불)을 역순으로 실행하여 최종 데이터 일관성을 회복합니다.

💡 프로덕션 사가 아키텍처 3대 필수 설계 원칙:<br> 1. 보상 액션의 실패 불가성(Pivot Step & Retry): 보상 트랜잭션 자체는 실패해서는 안 됩니다. 일시적 네트워크 장애로 보상이 실패할 경우 성공할 때까지 지수 백오프로 재시도되어야 합니다.<br> 2. 서비스별 멱등성(Idempotency) 보장: 네트워크 재시도로 동일한 사가 커맨드나 보상 요청이 중복 유입될 수 있으므로, 모든 참여 마이크로서비스는 사가 트랜잭션 ID를 멱등성 키로 검증해야 합니다.<br> 3. 시맨틱 락(Semantic Lock)을 통한 Dirty Read 방어: 사가는 로컬 트랜잭션을 커밋하므로 중간 상태의 데이터가 타 사용자에게 노출될 수 있습니다. 비즈니스 상태를 PENDING으로 마킹하여 미완료 데이터의 비인가 수정을 방어해야 합니다.<br>

3. 결론 및 도입 성과

사가 패턴은 고비용의 2PC 분산 락을 배제하고, 마이크로서비스 간 느슨한 결합(Loose Coupling)과 독립적인 배포 가용성을 유지하면서도 복잡한 분산 업무의 트랜잭션 무결성을 지켜내는 최선의 아키텍처입니다. 정밀한 상태 머신과 보상 트랜잭션의 멱등 설계를 결합함으로써 엔터프라이즈 환경에서 데이터 유실 없는 안정적인 분산 비즈니스를 완결할 수 있습니다.


출처 및 1차 공식 기술 문서

Similar Posts

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다