Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ordering System

시스템 개요

  • 상품에 대한 주문 및 환불, 그리고 재고량 조정 시 동시성을 고려하여 안전하게 처리하는 주문 시스템.

기능 요약

  • 재고의 권위(authority)는 Redis가 가지며(원자 연산), DB 재고는 아웃박스(Outbox) 패턴으로 사후 동기화됩니다. 주문 생성은 재고를 검증하지 않고 즉시 PENDING으로 응답한 뒤, 접수 및 완료·취소는 커밋 이후 비동기 방식으로 처리됩니다.

기술 스택

  • Java 17, Spring Boot (Web, Data JPA, Data Redis)
  • Querydsl (동적 조회), H2 (인메모리, MODE=MySQL), Redis (재고 권위 저장소)
  • p6spy (SQL 로깅), springdoc-openapi (Swagger)
  • k6 (부하 테스트)

실행 방법

1) Redis 구동

docker compose -f src/main/resources/docker/docker-compose.yml up -d redis

2) 어플리케이션 실행

./mvnw spring-boot:run
항목
JDBC URL jdbc:h2:mem:ordering;DB_CLOSE_DELAY=-1;MODE=MySQL
User Name sa

3) Redis 데이터 초기화 (어플리케이션 재실행 시)

DB는 create-drop + data.sql로 매 기동 시 시드값이 초기화되지만 Redis 재고 키는 이전 상태로 유지되어있으므로, 재실행 시 수동으로 초기화해야합니다.

docker exec redis redis-cli FLUSHALL

4) Redis 중단

docker compose -f src/main/resources/docker/docker-compose.yml down -v

부하 테스트 (k6)

여러 주문이 동시에 발생해도 상품 재고가 정확하게 차감되는 지 확인합니다.

k6 run k6/order-load.js

결과

Image Image

주문 처리 흐름

[POST /api/orders]
   └─ createOrder (TX)  : 주문을 PENDING 으로 저장만 한다 (재고 미검증)  →  즉시 201 응답
        └─ commit 후 OrderCreatedEvent 발행 (AFTER_COMMIT, @Async 백그라운드)
             ├─ acceptOrder   (REQUIRES_NEW)  : PENDING → ACCEPTED (취소 가능 상태로 먼저 커밋)
             └─ processOrder  (REQUIRES_NEW)  : 레디스 재고량 차감 (reserve)
                   ├─ 성공 → ACCEPTED → COMPLETED, 아웃박스에 DB 재고 -delta 기록
                   └─ 실패(재고 부족) → 롤백 → cancelOrder(REQUIRES_NEW) 로 ACCEPTED → CANCELLED

[POST /api/orders/{id}/refund]  : COMPLETED → CANCELLED, 재고 복구(+delta), 아웃박스 +delta
[POST /api/products/{id}/stock-adjust]  : 재고 델타 조정(+보충/−보정), Redis 즉시 반영 + 아웃박스 동기화

상태 전이 규칙: PENDING → ACCEPTED → (COMPLETED | CANCELLED), COMPLETED → CANCELLED(환불).


1. 동시성 및 트랜잭션 고려

1-1. 재고 차감의 원자성 — Redis Lua (all-or-nothing)

재고의 동시성 정합성은 전적으로 Redis 원자 연산으로 보장합니다. 한 주문에 여러 상품이 있을 때, 모든 상품의 재고를 한 번에 검사하고 차감해야 일부만 차감되는 부분 실패가 없습니다.

1-2. 트랜잭션 경계 분리 — 주문 생성과 재고 차감의 분리

주문 생성 트랜잭션은 주문 상태를 PENDING 으로 저장만 하고 커밋합니다. 재고 차감과 완료/취소 확정은 커밋 이후로 미룹니다(@TransactionalEventListener(AFTER_COMMIT) + @Async).

이렇게 한 이유:

  • 응답 지연 최소화: 무거운 재고 처리를 응답 로직에서 분리해 주문 생성 요청은 즉시 반환할 수 있습니다.
  • 롤백된 주문 처리 방지: 실제로 커밋된(AFTER_COMMIT) 주문만 후속 처리할 수 있습니다.

후속 처리에서 접수/완료/취소를 각각 별도 트랜잭션(REQUIRES_NEW)으로 나눈 이유:

  1. 한 트랜잭션이 롤백되면 그 안의 상태 변경도 함께 롤백되므로, 취소는 처리 트랜잭션과 분리되어야 합니다.
  2. 규칙상 PENDING에서는 바로 취소할 수 없으므로, 취소가 가능한 ACCEPTED 상태로 먼저 커밋해둡니다. 그래야 처리가 재고 부족으로 롤백돼도 ACCEPTED → CANCELLED로 취소할 수 있습니다.

1-3. Redis(비트랜잭션)와 DB(트랜잭션)의 정합성

Redis 차감은 트랜잭션과 무관하게 즉시 반영되므로, JPA 트랜잭션의 롤백/커밋과 어긋날 수 있습니다. 이를 Transaction Synchronization Hook 으로 정확히 1회만 보정합니다.

  • 주문 처리(processOrder): 재고를 먼저 예약(차감)한 뒤 afterCompletion보상(복원) 을 등록합니다. 트랜잭션이 롤백되면(예외 또는 커밋 단계 실패) Redis 예약을 복원합니다. 단순 try/catch는 커밋 단계에서의 실패를 알지 못하므로 afterCompletion(STATUS_ROLLED_BACK)을 사용합니다.
  • 환불(refundOrder): 재고 복구를 afterCommit으로 미룹니다. 커밋이 성공했을 때만 복구하여, 커밋 단계 실패로 주문 취소가 롤백됐는데 Redis만 과다 복구되는(초과 판매) 상황을 막습니다.

1-4. DB 재고 동기화 — Outbox 패턴 (정확히 1회)

DB의 stockQuantity는 Redis 재고량을 아웃박스 이벤트로 뒤따라 반영합니다.


2. 성능 고려

2-1. 구현된 성능 설계

# 설계 내용
1 DB 행 락 경합 제거 재고를 처리하는데 있어서 RDB 행 락(SELECT ... FOR UPDATE)으로 다투지 않고 Redis 인메모리 원자 연산으로 처리합니다. 특정 상품에 주문이 몰려도 DB 락 직렬화 병목이 없습니다.
2 선 응답 + 비동기 처리 주문 생성은 PENDING으로 즉시 반환하고, 재고 차감·완료 확정은 백그라운드(@Async)에서 처리합니다.
3 N+1 쿼리 방지 단건 조회는 fetchJoin(findByIdWithProducts)으로 주문–주문상품–상품을 한 번에 로딩하고, 컬렉션은 default_batch_fetch_size=100으로 IN 절을 사용하여 배치 로딩합니다.
4 재고 배치 조회 (MGET) 상품 목록 응답에서 항목마다 Redis를 호출하지 않고, 페이지 내 모든 상품 ID를 모아 StockService.getStocks(Redis MGET)로 한 번에 조회합니다. 상품 수에 비례하던 Redis 왕복(N회)을 1회+@로 줄입니다.

2-2. 주문 데이터가 많아질 때의 문제점과 개선 방안

문제점 개선 방안
@Async 풀 한계 — 이벤트 처리 풀이 버티지 못할 정도로 주문이 폭주하면 큐 포화로 후속 처리(완료 확정)가 지연될 수 있습니다. 워크로드에 맞춰 풀과 큐를 튜닝하고, 큐가 가득 차면 거부정책을 AbortPolicy가 아닌 CallerRunsPolicy로 두어 유실 없이 역압을 겁니다 (단, 포화 시 제출 스레드=요청 스레드가 직접 처리하므로 생성 응답은 느려집니다). 인메모리 큐의 유실 위험을 없애고 수평 확장·내구성이 필요하다면 외부 큐(Kafka 등)로 위임합니다.

About

주문 시스템

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages