프로젝트 목록

StockRush

주문·재고·결제를 분리한 커머스 백엔드입니다. Saga와 Transactional Outbox로 결제 실패 시 보상 처리, 중복 이벤트 처리 방지, 발행 실패 후 재처리 흐름을 구현했습니다.

개인 프로젝트 · 백엔드 설계·구현·테스트 · 2026 · 백엔드

역할
개인 프로젝트 · 백엔드 설계·구현·테스트
기간
2026
분야
백엔드
코드·테스트
주요 코드 3개 · 관련 테스트 3개

해결한 문제와 담당 역할

주문 저장, 재고 예약, 결제 승인과 이벤트 발행은 서비스 경계 때문에 하나의 DB 트랜잭션으로 처리할 수 없습니다. 부분 실패와 이벤트 재전달에 대비해 상태 전이, 보상 처리와 재발행을 구분해야 합니다.

게이트웨이와 도메인별 서비스를 분리하고 서비스별 PostgreSQL 스키마를 구성했습니다.

주문 Saga, 서비스별 Outbox 발행기와 발행 실패 이벤트의 관리자 재처리 API를 구현했습니다.

게이트웨이의 OIDC/JWT 인증과 서비스의 리소스 소유권 검사를 분리했습니다.

핵심 요구사항

  • 같은 멱등 키로 주문을 다시 보내면 새로 만들지 않고 처음 만든 주문을 돌려줍니다.
  • 취소되거나 확정된 주문은 뒤늦게 온 이벤트로 상태가 바뀌지 않습니다.
  • 끝내 발행하지 못한 이벤트도 지우지 않고 FAILED로 남겨 둡니다.
  1. 게이트웨이 인증
  2. 주문·Outbox 저장
  3. Kafka 이벤트 발행
  4. 재고·결제 처리
  5. 조회 모델 갱신
Gateway에서 주문 Saga, 서비스별 Outbox와 Kafka, 조회 모델로 이어지는 StockRush 구성도. 주요 흐름은 게이트웨이 인증 → 주문·Outbox 저장 → Kafka 이벤트 발행 → 재고·결제 처리 → 조회 모델 갱신 순서로 이어집니다.

기술 선택과 이유

주문 서비스가 흐름을 지휘

적용 방식
재고, 결제, 쿠폰, 출고의 다음 단계는 주문 서비스가 정합니다.
검토한 대안
각 서비스가 이벤트를 받아 다음 서비스로 알아서 넘기는 방식
선택 이유
다음 단계와 보상 순서를 주문 서비스에 모아 상태 전이를 추적하기 위한 선택입니다. 각 서비스의 처리 결과와 Outbox 상태도 함께 확인해야 합니다.

이벤트는 DB에 먼저 기록

적용 방식
주문을 저장할 때 보낼 이벤트도 같은 트랜잭션으로 Outbox 테이블에 넣습니다.
검토한 대안
커밋한 다음 코드에서 바로 Kafka로 보내기
선택 이유
DB 커밋과 Kafka 발행 사이에 실패하더라도 발행할 이벤트가 DB에 남아 재시도할 수 있습니다.

권한 확인은 두 번

적용 방식
게이트웨이에서 로그인을 확인하고, 서비스에서 주문 주인을 다시 확인합니다.
검토한 대안
게이트웨이를 통과했으면 믿기
선택 이유
게이트웨이를 거치지 않는 내부 호출이 생기면 남의 주문도 조회할 수 있게 됩니다.

주요 구현과 관련 테스트

핵심 로직과 동작을 확인한 테스트를 함께 정리했습니다.

주요 구현

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

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

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로 바뀝니다.

테스트 결과

테스트 방법 보기
StockRush 테스트 결과
구분테스트 방법결과
통합 테스트PostgreSQL 통합 테스트에서 같은 이벤트를 두 번 처리합니다.Outbox에는 1건만 남습니다.
통합 테스트주문을 취소한 다음 결제 승인 이벤트를 보냅니다.업데이트가 0건이라 주문은 취소 상태 그대로입니다.
통합 테스트항상 실패하는 발행기를 넣고 재시도를 끝까지 돌립니다.다섯 번째 실패 뒤 FAILED로 바뀝니다.

프로젝트 범위

검증 한계와 미구현 범위

  • 결제·출고는 내부 시뮬레이션입니다. 실제 PG·물류 연동과 운영 규모의 처리량·지연 시간은 검증하지 않았습니다.
  • Kafka 중단·재개는 로컬 단일 브로커 시나리오 범위이며, 장기 장애와 발행기 고가용성은 검증하지 않았습니다.

향후 개선

  • OpenTelemetry로 주문 하나가 서비스를 거치는 경로를 추적하고, Kafka가 오래 멈췄다가 살아났을 때 복구에 얼마나 걸리는지 재 보려고 합니다.

관련 자료

ADR 테스트 소스