Saga 패턴
2025. 11. 25. 03:27

MSA 환경에서 분산 트랜잭션 문제와 Saga 패턴

1. 문제 상황 파악

우리 프로젝트의 현재 상황

현재 우리 프로젝트는 MSA 구조로 다음과 같이 구성되어 있습니다:

1️⃣ 하나의 DB + 논리적 스키마 분리 (현재 개발 환경)

PostgreSQL (logistics)
├── order_schema
├── inventory_schema
└── delivery_schema

  • ✅ 별도의 트랜잭션으로 작동
  • ❌ Order 서비스가 롤백되어도 Inventory 서비스는 롤백 안 됨
  • ❌ DB가 다운되면 모든 서비스 마비

2️⃣ 여러 개의 DB 인스턴스 (운영 환경 목표)

Order DB → Order Service
Inventory DB → Inventory Service
Delivery DB → Delivery Service

  • ✅ 별도의 트랜잭션으로 작동
  • ❌ Order 서비스가 롤백되어도 Inventory 서비스는 롤백 안 됨
  • ✅ 하나의 DB가 다운되어도 다른 서비스는 정상 동작

3️⃣ 하나의 DB + 동일 트랜잭션 (모놀리스)

  • ✅ 롤백 시 모든 로직이 함께 롤백
  • ❌ MSA가 아님

핵심 문제: 데이터 일관성

현재 주문 생성 플로우는 다음과 같습니다:

주문 요청
  → 상품 조회 API 호출
  → 재고 차감 API 호출
  → 주문 저장
  → 배송 생성 API 호출
  (+ 향후 알림, AI 서비스 추가 예정)

문제 시나리오:

1. 재고 차감 성공 ✅ (Inventory DB에 COMMIT)
2. 주문 저장 성공 ✅ (Order DB에 COMMIT)
3. 배송 생성 실패 ❌

결과: 재고는 차감되고 주문은 생성되었는데 배송은 없음
→ 데이터 일관성 깨짐!

이런 분산 트랜잭션 문제를 해결하기 위해 Saga 패턴을 도입합니다.


2. Saga 패턴이란?

Saga 패턴은 MSA 환경에서 분산 트랜잭션의 데이터 일관성을 보장하기 위한 패턴입니다.

핵심 개념

  1. 각 서비스의 로컬 트랜잭션을 순차적으로 처리
  2. 각 트랜잭션이 완료되면 다음 트랜잭션을 트리거
  3. 실패 시 보상 트랜잭션(Compensating Transaction)을 실행

보상 트랜잭션이란?

처음에는 "이전 트랜잭션을 롤백하는 건가?"라고 생각했지만, 그게 아닙니다!

❌ 롤백 (ROLLBACK)

  • 같은 트랜잭션 내에서만 가능
  • 물리적으로 데이터를 취소

✅ 보상 (Compensation)

  • 이미 COMMIT된 트랜잭션을 논리적으로 취소
  • 반대 작업을 수행하는 새로운 트랜잭션

예시:

// 정방향 트랜잭션
재고 차감: stock = 10 → 5

// 보상 트랜잭션 (롤백이 아님!)
재고 복구: stock = 5 → 10 (새로운 트랜잭션으로 증가)


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

3-1. Choreography-based Saga (코레오그래피)

특징:

  • 중앙 컨트롤러 없이 각 서비스가 이벤트를 주고받음
  • 각 서비스가 독립적으로 다음 서비스를 트리거

동작 방식:

Order Service: 주문 생성 → OrderCreatedEvent 발행
    ↓
Inventory Service: 이벤트 수신 → 재고 차감 → InventoryDeductedEvent 발행
    ↓
Delivery Service: 이벤트 수신 → 배송 생성 → DeliveryCreatedEvent 발행

장점:

  • ✅ 구성과 운영이 간편
  • ✅ 서비스 간 결합도가 낮음
  • ✅ 단일 실패 지점(SPOF)이 없음

단점:

  • ❌ 전체 워크플로우 파악이 어려움
  • ❌ 서비스 간 순환 종속성 위험
  • ❌ 통합 테스트가 복잡함 (모든 서비스를 실행해야 함)

3-2. Orchestration-based Saga (오케스트레이션)

특징:

  • 중앙 집중식 컨트롤러(Orchestrator)가 모든 트랜잭션을 관리
  • Orchestrator가 각 서비스에 명령을 내리고 상태를 추적

동작 방식:

Orchestrator (예: OrderFacade)
  ├─ 1. 상품 조회 API 호출
  ├─ 2. 재고 차감 API 호출
  ├─ 3. 주문 생성
  ├─ 4. 배송 생성 API 호출
  └─ 실패 시: 보상 트랜잭션 순차 실행

장점:

  • ✅ 한 곳에서 전체 플로우 파악 가능
  • ✅ 순환 종속성 방지
  • ✅ 복잡한 비즈니스 로직 관리 용이
  • ✅ 디버깅과 모니터링이 쉬움

단점:

  • ❌ Orchestrator가 단일 실패 지점이 될 수 있음
  • ❌ 중앙 집중화로 인한 병목 가능성
  • ❌ Orchestrator 구현 복잡도 증가

4. 우리 팀의 선택: Orchestration

  • 튜터님이 Choreography-based Saga 패턴을 추천해주었기 때문에 우리는 청개구리여서 반대로 선택하였습니다.
  • 여기까지 읽어보신다면 소소한 재미를 위해 Joke 한번 넣었습니다.

왜 Orchestration을 선택했는가?

현재 우리 코드를 보면 이미 Orchestration 패턴의 기초가 구현되어 있습니다:

@Component
public class OrderFacade {

    @Transactional
    public Order createOrder(CreateOrderCommand command) {
        // 1. 상품 정보 조회
        Map<UUID, GetProductResponse> productInfoMap = fetchProductInfos(command);

        // 2. 주문 생성
        Order order = orderService.createOrder(command);

        // 3. 재고 차감
        inventoryIntegrationService.deductInventory(...);

        // 4. 배송 생성
        deliveryIntegrationService.createDelivery(...);

        return order;
    }
}

OrderFacade가 이미 중앙 조율자 역할을 수행하고 있습니다!

선택 기준

기준 Choreography Orchestration 우리 선택

제어 방식 이벤트 체인 명시적 순서 제어 ✅ 명시적
전체 플로우 파악 여러 서비스 코드 확인 필요 한 곳에서 파악 가능 ✅ 한 곳
현재 구조 이벤트 기반 필요 Facade 이미 존재 ✅ Facade 있음
서비스 개수 많을수록 유리 적을수록 유리 ✅ 3~5개 예상
비즈니스 로직 단순 복잡 ✅ 복잡

결론

  1. 이미 OrderFacade가 순서를 제어하고 있음
  2. 한 곳에서 전체 플로우를 파악할 수 있어 유지보수 용이
  3. 서비스 개수가 적음 (Order, Inventory, Delivery + α)
  4. 주문 생성은 순서가 중요하고 복잡한 비즈니스 로직

따라서 Orchestration-based Saga 패턴을 선택했습니다!



5. 핵심 포인트 정리

Saga 패턴 구현 시 주의사항

1. 멱등성 보장 (Idempotency)

같은 요청이 여러 번 와도 결과가 동일해야 합니다.

// 나쁜 예
public void deductInventory(UUID productId, int quantity) {
    product.decreaseStock(quantity);  // 여러 번 호출하면 계속 차감!
}

// 좋은 예
public void deductInventory(String idempotencyKey, UUID productId, int quantity) {
    if (alreadyProcessed(idempotencyKey)) {
        return;  // 이미 처리됨
    }
    product.decreaseStock(quantity);
    saveIdempotencyKey(idempotencyKey);
}

2. 보상 트랜잭션은 역순으로

정방향과 반대 순서로 실행합니다.

정방향: 주문 생성 → 재고 차감 → 배송 생성
보상:   배송 취소 → 재고 복구 → 주문 취소

3. 보상 트랜잭션도 실패할 수 있음

보상 트랜잭션 실패에 대비한 재시도 메커니즘이 필요합니다.

try {
    restoreInventory();
} catch (Exception e) {
    // 재시도 큐에 추가
    retryQueue.add(new RetryTask("restoreInventory", orderId));
    // 운영팀에 알림
    alertService.send("보상 트랜잭션 실패", orderId);
}

4. 타임아웃 설정

각 단계마다 적절한 타임아웃을 설정해 무한 대기를 방지합니다.

@FeignClient(
    name = "inventory-service",
    configuration = FeignConfig.class
)
public interface InventoryClient {

    @PostMapping("/v1/inventories/deduct")
    @Timeout(value = 5000)  // 5초 타임아웃
    DeductInventoryResponse deductInventory(@RequestBody DeductInventoryRequest request);
}


6. 향후 확장 계획

Kafka 도입 시

현재 동기 방식에서 비동기 메시징으로 전환 가능합니다:

// 현재: 동기 방식 (HTTP/Feign)
inventoryClient.deductInventory(request);

// 향후: Kafka 비동기 방식
kafkaProducer.send("inventory.deduct", request);

// 결과는 이벤트로 수신
@KafkaListener(topics = "inventory.deducted")
public void onInventoryDeducted(InventoryDeductedEvent event) {
    // 다음 단계 진행
}

장점:

  • Orchestrator는 유지하되, 통신 방식만 변경
  • 각 서비스의 독립성 향상
  • 비동기 처리로 성능 개선

알림/AI 서비스 추가

새로운 서비스가 추가되어도 Orchestrator에 단계만 추가하면 됩니다:

public Order createOrder(CreateOrderCommand command) {
    try {
        // 기존 로직
        order = orderService.createOrder(command);
        inventoryIntegrationService.deductInventory(...);
        deliveryIntegrationService.createDelivery(...);

        // 5. 알림 발송 (신규)
        notificationService.sendOrderConfirmation(order);

        // 6. AI 분석 (신규)
        aiService.analyzeOrder(order);

    } catch (Exception e) {
        // 보상 트랜잭션에도 추가
        compensate(order, ...);
    }
}



7. 결론

MSA 환경에서는 분산 트랜잭션 문제가 필연적으로 발생합니다. 우리 팀은:

  1. 문제 인식: 서비스 간 트랜잭션 롤백 불가능
  2. 해결 방안: Saga 패턴 도입
  3. 구현 방식: Orchestration-based Saga
  4. 선택 이유:
    • 현재 구조(OrderFacade)와의 정합성
    • 전체 플로우의 가시성
    • 복잡한 비즈니스 로직 관리 용이
    • 유지보수 편의성

Saga 패턴은 완벽한 원자성을 보장하지는 못하지만, 최종 일관성(Eventual Consistency)을 통해 분산 시스템의 데이터 일관성을 유지합니다.

핵심 요약

문제 MSA에서 분산 트랜잭션 롤백 불가
해결책 Saga 패턴 (보상 트랜잭션)
방식 Orchestration (중앙 제어)
장점 가시성, 유지보수성, 명확한 플로우
주의사항 멱등성, 역순 보상, 재시도

8. 참고 자료


'👍코드 회고' 카테고리의 다른 글

Outbox와 DLQ 아키텍처 설계  (0) 2025.12.01
Saga 패턴2  (0) 2025.11.27
N+1 (10.17)  (0) 2025.10.17
리팩토링 vs 오버 엔지니어링 (10.16)  (0) 2025.10.16
장바구니 코드 속 store_id는 어디에 둘까? (10.15)  (0) 2025.10.15