카프카 심화편 (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
시리즈 다른 편
- 1편 — EDA·Kafka 기초·KRaft
- 2편 — Topic·Partition·Offset
- 3편 — Producer·Consumer 동작
- 4편 — Consumer Group·리밸런싱
- 5편 — Reactor Kafka
- 6편 — Cluster·HA·Best Practices
- 7편 — 배치·에러·트랜잭션
- 8편 — Spring Kafka·테스트·보안
- 9편 — Spring Cloud Stream 기초
- 10편 — StreamBridge 동적 라우팅
- 11편 — Fan-Out / Fan-In
- 12편 — SCS Tips & Tricks
- 13편 — Saga 코레오그래피 (현재 글)
- 14편 — Saga 오케스트레이터
- 15편 — Transactional Outbox
공식 문서: Microservices Patterns — Saga 에서 더 깊이.
다음 글(6편)에서는 Saga Orchestrator — 중앙 코디네이터가 흐름 제어, 명령/응답 토픽, 워크플로우 상태 머신, 코레오그래피와의 결정적 차이까지 풀어 갑니다.