해결한 문제와 담당 역할
주문 저장, 재고 예약, 결제 승인과 이벤트 발행은 서비스 경계 때문에 하나의 DB 트랜잭션으로 처리할 수 없습니다. 부분 실패와 이벤트 재전달에 대비해 상태 전이, 보상 처리와 재발행을 구분해야 합니다.
게이트웨이와 도메인별 서비스를 분리하고 서비스별 PostgreSQL 스키마를 구성했습니다.
주문 Saga, 서비스별 Outbox 발행기와 발행 실패 이벤트의 관리자 재처리 API를 구현했습니다.
게이트웨이의 OIDC/JWT 인증과 서비스의 리소스 소유권 검사를 분리했습니다.
핵심 요구사항
- 같은 멱등 키로 주문을 다시 보내면 새로 만들지 않고 처음 만든 주문을 돌려줍니다.
- 취소되거나 확정된 주문은 뒤늦게 온 이벤트로 상태가 바뀌지 않습니다.
- 끝내 발행하지 못한 이벤트도 지우지 않고 FAILED로 남겨 둡니다.
- 게이트웨이 인증
- 주문·Outbox 저장
- Kafka 이벤트 발행
- 재고·결제 처리
- 조회 모델 갱신
기술 선택과 이유
주문 서비스가 흐름을 지휘
- 적용 방식
- 재고, 결제, 쿠폰, 출고의 다음 단계는 주문 서비스가 정합니다.
- 검토한 대안
- 각 서비스가 이벤트를 받아 다음 서비스로 알아서 넘기는 방식
- 선택 이유
- 다음 단계와 보상 순서를 주문 서비스에 모아 상태 전이를 추적하기 위한 선택입니다. 각 서비스의 처리 결과와 Outbox 상태도 함께 확인해야 합니다.
이벤트는 DB에 먼저 기록
- 적용 방식
- 주문을 저장할 때 보낼 이벤트도 같은 트랜잭션으로 Outbox 테이블에 넣습니다.
- 검토한 대안
- 커밋한 다음 코드에서 바로 Kafka로 보내기
- 선택 이유
- DB 커밋과 Kafka 발행 사이에 실패하더라도 발행할 이벤트가 DB에 남아 재시도할 수 있습니다.
권한 확인은 두 번
- 적용 방식
- 게이트웨이에서 로그인을 확인하고, 서비스에서 주문 주인을 다시 확인합니다.
- 검토한 대안
- 게이트웨이를 통과했으면 믿기
- 선택 이유
- 게이트웨이를 거치지 않는 내부 호출이 생기면 남의 주문도 조회할 수 있게 됩니다.
주요 구현과 관련 테스트
핵심 로직과 동작을 확인한 테스트를 함께 정리했습니다.
주요 구현
PersistentCreateOrderService.create
services/order-service/src/main/java/com/stockrush/order/application/PersistentCreateOrderService.java
주요 구현
PersistentCreateOrderService.create
services/order-service/src/main/java/com/stockrush/order/application/PersistentCreateOrderService.java코드
소스 파일 열기boolean saved = orderRepository.saveIfAbsent(result.order(), command.idempotencyKey());
if (!saved) {
return CreateOrderResult.replayed(
replayOrder(command.idempotencyKey(), command.memberId())
);
}
outboxEventRepository.save(result.outboxEvent());
구현 내용
주문과 Outbox를 한 트랜잭션에 저장합니다. 같은 멱등 키가 이미 있으면 저장하지 않고 기존 주문을 찾아 돌려줍니다.
관련 테스트
PersistentCreateOrderServiceIntegrationTest.persists_order_items_and_pending_outbox_event_togetherservices/order-service/src/test/java/com/stockrush/order/infra/persistence/PersistentCreateOrderServiceIntegrationTest.java테스트 결과Outbox에는 1건만 남습니다.
추가 구현
OrderSagaEventHandler.transitionOrder
services/order-service/src/main/java/com/stockrush/order/application/OrderSagaEventHandler.java
추가 구현
OrderSagaEventHandler.transitionOrder
services/order-service/src/main/java/com/stockrush/order/application/OrderSagaEventHandler.java코드
소스 파일 열기where order_id = :orderId
and status not in ('CANCELLED', 'CONFIRMED')
return updated == 1;
구현 내용
WHERE 조건에서 취소, 확정 주문을 빼 두었기 때문에 늦게 온 이벤트는 0건 업데이트로 끝납니다.
관련 테스트
OrderSagaEventHandlerIntegrationTest.ignores_payment_authorized_for_cancelled_orderservices/order-service/src/test/java/com/stockrush/order/application/OrderSagaEventHandlerIntegrationTest.java테스트 결과업데이트가 0건이라 주문은 취소 상태 그대로입니다.
추가 구현
OutboxRelayService.claimPending
services/order-service/src/main/java/com/stockrush/order/infra/outbox/OutboxRelayService.java
추가 구현
OutboxRelayService.claimPending
services/order-service/src/main/java/com/stockrush/order/infra/outbox/OutboxRelayService.java코드
소스 파일 열기where status = 'PENDING'
and (next_retry_at is null or next_retry_at <= now())
order by created_at, id
limit :batchSize
for update skip locked
구현 내용
SKIP LOCKED로 잠긴 행을 건너뛰고 선택한 이벤트를 PUBLISHING으로 전환합니다. 연결 테스트는 재시도 한도 초과 후 FAILED 전환을 확인합니다.
관련 테스트
OutboxRelayServiceIntegrationTest.marks_failed_when_publish_retry_count_is_exhaustedservices/order-service/src/test/java/com/stockrush/order/infra/outbox/OutboxRelayServiceIntegrationTest.java테스트 결과다섯 번째 실패 뒤 FAILED로 바뀝니다.
테스트 결과
같은 재고 이벤트를 2번 보냄
다음 단계 이벤트는 1건만 생김
관련 코드OrderSagaEventHandlerIntegrationTest.ignores_duplicate_inventory_event 취소 뒤 결제 승인 도착
주문은 그대로 취소 상태
관련 코드OrderSagaEventHandlerIntegrationTest.ignores_payment_authorized_for_cancelled_order 발행 5번 연속 실패
지우지 않고 FAILED로 남겨 다시 보낼 수 있음
관련 코드OutboxRelayServiceIntegrationTest.marks_failed_when_publish_retry_count_is_exhausted 테스트 방법 보기
| 구분 | 테스트 방법 | 결과 |
|---|---|---|
| 통합 테스트 | PostgreSQL 통합 테스트에서 같은 이벤트를 두 번 처리합니다. | Outbox에는 1건만 남습니다. |
| 통합 테스트 | 주문을 취소한 다음 결제 승인 이벤트를 보냅니다. | 업데이트가 0건이라 주문은 취소 상태 그대로입니다. |
| 통합 테스트 | 항상 실패하는 발행기를 넣고 재시도를 끝까지 돌립니다. | 다섯 번째 실패 뒤 FAILED로 바뀝니다. |
프로젝트 범위
검증 한계와 미구현 범위
- 결제·출고는 내부 시뮬레이션입니다. 실제 PG·물류 연동과 운영 규모의 처리량·지연 시간은 검증하지 않았습니다.
- Kafka 중단·재개는 로컬 단일 브로커 시나리오 범위이며, 장기 장애와 발행기 고가용성은 검증하지 않았습니다.
향후 개선
- OpenTelemetry로 주문 하나가 서비스를 거치는 경로를 추적하고, Kafka가 오래 멈췄다가 살아났을 때 복구에 얼마나 걸리는지 재 보려고 합니다.
관련 자료
