Kafka 마스터 — Saga Choreography (코레오그래피)

2026-05-03확률과 통계 마스터 노트

카프카 심화편 (9~15편) 5편. 분산 트랜잭션의 어려움과 Saga 패턴이 푸는 방식, 코레오그래피 vs 오케스트레이터 구현 비교, 중앙 관제 없이 이벤트로만 협력하는 흐름, Happy Path(OrderCreated → Payment + Inventory → OrderCompleted → Shipping), Negative Path(InventoryDeclined → OrderCanceled → 보상), sealed interface로 이벤트 타입 안전성, 멱등 처리까지.

이 글은 카프카 마스터 노트 시리즈의 열세 번째 편입니다. 1~4편이 SCS 도구였다면, 이번엔 마이크로서비스 분산 트랜잭션의 핵심 패턴 — Saga, 그 첫 구현 코레오그래피.

여러 서비스가 협력해 비즈니스 트랜잭션을 완성. 결제·재고·배송이 다 성공해야 주문 완료. 한 곳 실패하면? 이미 성공한 것들을 되돌려야 합니다. 이걸 어떻게 우아하게? Saga.

처음 Saga 코레오그래피가 어렵게 느껴지는 이유

처음 이 단원이 어렵게 느껴지는 이유는 두 가지예요. 첫째, "중앙 관제 없이"가 막연합니다. 누가 결과를 모으나? 누가 실패를 감지하나? 둘째, 이벤트 타입이 너무 많아 보입니다. OrderCreated·PaymentDeducted·InventoryDeclined·OrderCanceled·PaymentRefunded… 흐름이 안 잡힙니다.

해결법은 한 가지예요. "이메일 체인" 비유로 묶는 것. 코레오그래피 = 이메일 CC로 모두에게 알림 → 각자 알아서 반응. Happy Path = 모두 OK 답장. Negative Path = 한 명 거절 → 도미노로 취소 알림. 이 그림이 잡히면 흐름이 보입니다.

분산 트랜잭션 문제

전통 모놀리스:

BEGIN TRANSACTION;
  INSERT INTO orders ...;
  UPDATE accounts SET balance = balance - 100 WHERE ...;
  UPDATE inventory SET stock = stock - 1 WHERE ...;
  INSERT INTO shipping ...;
COMMIT;

ACID 보장. 한 줄 실패 → ROLLBACK.

마이크로서비스:

Order Service ─→ DB 1 (orders)
Payment Service ─→ DB 2 (accounts)
Inventory Service ─→ DB 3 (stock)
Shipping Service ─→ DB 4 (shipments)

각 서비스 별도 DB. 분산 ACID 트랜잭션 X (또는 비싼 2PC). 그래서 Saga.

Saga 패턴 — 핵심 아이디어

각 단계를 독립 트랜잭션 + 보상 트랜잭션으로:

주문 처리:
  1. 주문 생성 → (실패 시: 취소)
  2. 결제 → (실패 시: 환불)
  3. 재고 차감 → (실패 시: 복구)
  4. 배송 → (실패 시: 취소)

각 단계 = 자기 DB에서 자기 트랜잭션
보상 = 실패 시 이미 성공한 것 되돌림

여기서 정말 중요한 시험 함정 — Saga는 ACID 아님, BASE. Eventual consistency. 중간 상태에선 일관성 깨질 수 있음. 보상 가능한 비즈니스에만 적용 (송금·주문 OK / 의료 처방 X).

두 가지 Saga 구현

구현 설명
Choreography (코레오그래피) 중앙 관제 X, 이벤트로만 협력
Orchestrator (오케스트레이터) 중앙 코디네이터가 흐름 제어

이번 편 = 코레오그래피. 6편 = 오케스트레이터.

코레오그래피 큰 그림

[Client] POST /orders
   ↓
[Order Service] → OrderCreated → order-events
                                    ↓
        ┌───────────────┬────────────┴─────────────┐
        ↓               ↓                          ↓
[Payment Service]  [Inventory Service]       (다른 서비스)
   ↓                  ↓
PaymentDeducted    InventoryDeducted
   → payment-events  → inventory-events
        ↓               ↓
        └───────────────┴────────┐
                                  ↓
                         [Order Service] (구독)
                                  ↓
              둘 다 성공 → OrderCompleted → order-events
              하나라도 실패 → OrderCanceled → 보상

각 서비스가 자기 책임. Order Service는 결과를 모아 최종 상태 결정.

sealed interface로 이벤트 타입 안전

5편(1~8편)에서 본 sealed interface:

public sealed interface OrderEvent
    permits OrderCreated, OrderCompleted, OrderCanceled {
}

public record OrderCreated(String orderId, BigDecimal amount, String userId) implements OrderEvent {}
public record OrderCompleted(String orderId) implements OrderEvent {}
public record OrderCanceled(String orderId, String reason) implements OrderEvent {}

public sealed interface PaymentEvent
    permits PaymentDeducted, PaymentDeclined, PaymentRefunded {
}

// ...

장점:

  • 컴파일 타임 타입 안전
  • Switch 패턴 매칭 (모든 케이스 강제)
  • 명시적 도메인 모델링
String describe(OrderEvent event) {
    return switch (event) {
        case OrderCreated created -> "New order: " + created.orderId();
        case OrderCompleted completed -> "Done: " + completed.orderId();
        case OrderCanceled canceled -> "Cancel: " + canceled.reason();
    };
}

여기서 시험 함정이 하나 있어요. sealed interface = 이벤트 도메인의 표준. record와 결합 시 불변·간결·안전.

Happy Path

1. POST /orders
2. Order Service: 주문 생성, OrderCreated 발행
   → order-events 토픽

3. Payment Service: order-events 구독, OrderCreated 받음
   → 결제 시도, 성공
   → PaymentDeducted 발행 → payment-events 토픽

4. Inventory Service: order-events 구독, OrderCreated 받음
   → 재고 차감, 성공
   → InventoryDeducted 발행 → inventory-events 토픽

5. Order Service: payment-events·inventory-events 구독
   → 둘 다 성공 도착 → OrderCompleted 발행
   → order-events 토픽

6. Shipping Service: order-events 구독, OrderCompleted 받음
   → 배송 예약, ShippingScheduled 발행

각자 자기 도메인만 책임.

Negative Path — 보상 트랜잭션

1. OrderCreated → Payment OK + Inventory FAIL

2. Inventory Service: 재고 부족
   → InventoryDeclined 발행 → inventory-events

3. Order Service: InventoryDeclined 받음
   → OrderCanceled 발행 → order-events

4. Payment Service: OrderCanceled 받음 (자기 토픽 구독 + order-events 구독)
   → PaymentRefunded → payment-events

핵심 — 각 서비스가 OrderCanceled 보고 알아서 보상. 누가 시키지 않음. 코레오그래피의 본질.

여기서 정말 중요한 시험 함정 — 각 서비스의 책임 = 자기 도메인 + 자기 보상. Order Service가 Payment에 "환불해" 명령 X. OrderCanceled만 발행 → Payment Service가 알아서 환불 결정.

DB 스키마 — 1:1 분리

-- Order Service DB
CREATE TABLE purchase_order (
    order_id VARCHAR PRIMARY KEY,
    user_id VARCHAR,
    amount DECIMAL,
    status VARCHAR,
    created_at TIMESTAMP
);

-- Payment Service DB
CREATE TABLE order_payment (
    order_id VARCHAR PRIMARY KEY,    -- order의 ID와 1:1
    amount DECIMAL,
    status VARCHAR,
    processed_at TIMESTAMP
);

-- Inventory Service DB
CREATE TABLE order_inventory (
    order_id VARCHAR PRIMARY KEY,
    items JSON,
    status VARCHAR
);

각 서비스가 자기 테이블. order_id로 연결.

멱등 처리 — 코레오그래피의 핵심

같은 이벤트를 여러 번 받을 수 있음 (at-least-once):

@Transactional
public void onOrderCreated(OrderCreated event) {
    if (paymentRepo.existsByOrderId(event.orderId())) {
        log.info("Already processed: {}", event.orderId());
        return;
    }
    // 결제 처리 + DB 저장
    paymentRepo.save(...);
    publish(new PaymentDeducted(event.orderId(), ...));
}

여기서 정말 중요한 시험 함정 — 코레오그래피 = 멱등 처리 필수. 같은 OrderCreated 두 번 처리하면 결제 두 번. 처리 여부를 DB에 추적.

DomainEvent / Saga / Processor 계층

// 도메인 이벤트
public sealed interface DomainEvent permits OrderEvent, PaymentEvent, InventoryEvent {}

// Saga 인터페이스 (서비스별 구현)
public interface OrderEventProcessor {
    void process(OrderEvent event);
}

@Service
public class OrderProcessorImpl implements OrderEventProcessor {
    public void process(OrderEvent event) {
        switch (event) {
            case OrderCreated c -> handleCreated(c);
            case OrderCompleted c -> handleCompleted(c);
            case OrderCanceled c -> handleCanceled(c);
        }
    }
}

SCS 통합

@Bean
public Function<Flux<OrderEvent>, Flux<PaymentEvent>> paymentProcessor(
    PaymentService service) {
    return events -> events
        .filter(e -> e instanceof OrderCreated)
        .cast(OrderCreated.class)
        .flatMap(e -> service.processPayment(e), 8);
}
spring.cloud.stream.bindings.paymentProcessor-in-0:
  destination: order-events
  group: payment-service
spring.cloud.stream.bindings.paymentProcessor-out-0:
  destination: payment-events

코레오그래피 장단점

장점 단점
단순·결합도 낮음 흐름 추적 어려움
서비스 추가 쉬움 디버깅 복잡
중앙 SPOF 없음 순환 의존 위험
분산·확장 자연스러움 비즈니스 로직 분산

여기서 시험 함정이 하나 있어요. 코레오그래피 = 단순 흐름·소수 서비스에 적합. 5+ 서비스 + 복잡 분기 = 오케스트레이터(6편)가 더 좋음.

통합 테스트 전략

@SpringBootTest
@EmbeddedKafka(topics = {"order-events", "payment-events", "inventory-events"})
class SagaTest {
    @Test
    void happyPath() {
        // OrderCreated 발행
        publish("order-events", new OrderCreated(...));

        // 응답 토픽 검증
        StepVerifier.create(receiver.receive("payment-events").take(1))
            .expectNextMatches(e -> e.value() instanceof PaymentDeducted)
            .verifyComplete();
    }
}

시험 직전 한 번 더 — 자주 헷갈리는 함정 모음

여기까지가 5편의 핵심입니다. 시험 직전 또는 실무에서 헷갈릴 때 다시 펼쳐 볼 수 있게 압축 노트로 마무리할게요.

  • 분산 트랜잭션 = 마이크로서비스의 핵심 문제
  • 분산 ACID X (또는 비싼 2PC)
  • Saga = 단계 + 보상 트랜잭션
  • ACID X, BASE / Eventual Consistency
  • 보상 가능 비즈니스에만 적용
  • 두 구현 — Choreography / Orchestrator
  • Choreography = 중앙 관제 X, 이벤트로 협력
  • 각 서비스 자기 책임 + 자기 보상
  • Happy Path — OrderCreated → Payment+Inventory → OrderCompleted → Shipping
  • Negative Path — InventoryDeclined → OrderCanceled → PaymentRefunded
  • sealed interface + record = 이벤트 도메인 표준
  • 컴파일 타임 안전·Switch 패턴
  • DB 1:1 분리 — 각 서비스 자기 테이블
  • 멱등 처리 필수 (at-least-once)
  • 처리 여부 DB 추적
  • DomainEvent / Saga / Processor 계층
  • SCS Function으로 자연스러운 통합
  • 코레오그래피 장점 — 단순·결합 ↓·확장 ↑
  • 단점 — 흐름 추적·디버깅·순환 의존 위험
  • 소수 서비스 = 코레오그래피 / 5+ 복잡 분기 = 오케스트레이터
  • 통합 테스트 — Embedded Kafka + StepVerifier

시리즈 다른 편

공식 문서: Microservices Patterns — Saga 에서 더 깊이.

다음 글(6편)에서는 Saga Orchestrator — 중앙 코디네이터가 흐름 제어, 명령/응답 토픽, 워크플로우 상태 머신, 코레오그래피와의 결정적 차이까지 풀어 갑니다.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!