e-커머스 주문 처리 시나리오를 saga를 Orchestration 으로 구현한 예제입니다.
주문 → 재고 예약 → 결제 흐름을 order-app(오케스트레이터)이 Kafka command/reply로 순서대로 호출하고,
재고 부족 또는 결제 실패 시 이미 차감한 재고를 되돌리고 주문을 CANCELLED로 바꿉니다.
- Java 21, Spring Boot 3.5.13, Gradle (멀티모듈)
- Spring Kafka (command/reply 비동기 통신)
- Spring Data JPA + H2 (서비스별 인메모리 DB)
- 테스트: JUnit5, Mockito,
spring-kafka-test(EmbeddedKafka), Awaitility
3개 독립 마이크로서비스. 각 서비스 내부는 레이어드 아키텍처를 따릅니다.
controller # presentation (REST)
service # business (OrderSagaOrchestrator 포함)
repository # persistence (Spring Data JPA)
domain # JPA @Entity = 도메인 (분리 매핑 없음)
dto # web DTO + Kafka command/reply 메시지 (서비스별 중복 정의)
infrastructure
├── messaging # Kafka producer / consumer
└── config # KafkaConfig (토픽 선언)
| 서비스 | 포트 | 역할 |
|---|---|---|
order-app |
8080 | 오케스트레이터 + 주문/saga 상태 영속화 |
stock-app |
8081 | 재고 예약(차감)/복원 |
payment-app |
8082 | 결제 처리(결정론적 성공/실패) |
POST /orders → Order(PENDING) + OrderSaga(STARTED) 저장
└─ stock-command(RESERVE) ─▶ stock-app
├─ RESERVED ─────────▶ saga=STOCK_RESERVED
│ └─ payment-command(PAY) ─▶ payment-app
│ ├─ COMPLETED ─▶ saga=COMPLETED, Order=CONFIRMED (정상 흐름)
│ └─ FAILED ────▶ saga=COMPENSATING
│ └─ stock-command(RELEASE) ─▶ stock-app
│ └─ RELEASED ─▶ saga=COMPENSATED, Order=CANCELLED (보상)
└─ RESERVE_FAILED ───▶ saga=FAILED, Order=CANCELLED (재고 부족, 보상 없음)
| 토픽 | producer | consumer | 메시지 타입 |
|---|---|---|---|
stock-command |
order | stock | RESERVE, RELEASE |
stock-reply |
stock | order | RESERVED, RESERVE_FAILED, RELEASED |
payment-command |
order | payment | PAY |
payment-reply |
payment | order | COMPLETED, FAILED |
메시지는 JSON 문자열로 직렬화하며, 응답이 도착하면 sagaId로 어떤 주문의 saga인지 찾습니다.
- 재고 부족: 요청
quantity > 보유 재고→RESERVE_FAILED - 결제 실패: 요청
amount >= 100000(saga.payment.fail-threshold) →FAILED
초기 재고(stock-app/src/main/resources/data.sql): productId 1=10, 2=5, 3=0.
./gradlew test- 단위:
StockService(재고 부족 분기),PaymentService(실패 분기),OrderSagaOrchestrator(상태 전이/보상) - 통합(
@SpringBootTest+@EmbeddedKafka): 정상 흐름 / 재고 부족 / 결제 실패 보상 3 시나리오. stock·payment는OrderSagaIntegrationTest의SagaPartnerStub이 대신합니다.
docker-compose up -d # Kafka 기동 (localhost:9092)
./gradlew :stock-app:bootRun & # 8081
./gradlew :payment-app:bootRun & # 8082
./gradlew :order-app:bootRun & # 8080
# ① 정상 흐름 → CONFIRMED
curl -X POST localhost:8080/orders -H 'Content-Type: application/json' \
-d '{"productId":1,"quantity":1,"amount":1000}'
# ② 재고 부족 → CANCELLED (saga FAILED)
curl -X POST localhost:8080/orders -H 'Content-Type: application/json' \
-d '{"productId":1,"quantity":99999,"amount":1000}'
# ③ 결제 실패 → CANCELLED (saga COMPENSATED, 재고 원복)
curl -X POST localhost:8080/orders -H 'Content-Type: application/json' \
-d '{"productId":1,"quantity":1,"amount":100000}'
# 상태 조회
curl localhost:8080/orders/1주문 생성은 비동기 처리이므로 202 Accepted(PENDING)로 즉시 응답하며,
최종 상태는 GET /orders/{id}로 확인합니다. 각 단계 전이는 로그(SLF4J)와 H2 콘솔
(/h2-console)에서 추적할 수 있습니다.