테이블과 기본 인덱스, 재고 할당 API를 만든 V0에서 출발한 홈랩 프로젝트입니다. 동시성 제어 전략을 비교해 조건부 UPDATE를 선택한 뒤 DB 최적화, Redis 품절 처리, Kafka·Outbox와 장애 복구를 단계적으로 적용했습니다.
SCOPE
담당 업무 및 개발 내용
상품·창고·재고·주문·할당을 모듈형 단일 애플리케이션으로 구성했습니다. 최초 API의 초과 할당을 재현하고 실험 브랜치별로 동시성 전략을 비교했습니다. 선택한 방식 위에서 DB 접근을 줄이고 Redis와 이벤트 발행을 추가했으며, 마지막에는 중복 요청과 인프라 장애의 복구 경로를 실험했습니다.
실험 브랜치experiment/v1-optimisticexperiment/v1-pessimisticexperiment/v1-atomicexperiment/v1-optimistic-backoff-jitter
동시성 전략 비교 처리 흐름
전략별 구현낙관 · 비관 · Atomic
동일 부하100 VUs · 1,000요청
결과 비교정합성 · 예외 · 성능
방식 선택Early Atomic UPDATE
PROBLEM
문제
초과 할당을 막는 것과 요청에 정확한 품절 결과를 반환하는 것은 함께 해결해야 했습니다. 충돌 재시도와 DB 잠금 대기는 각각 다른 비용을 만들었습니다.
SOLUTION
해결
낙관적 락에는 새 트랜잭션에서 최대 3회 시도하는 재시도를 추가하고, 비관적 락과 조건부 UPDATE도 별도 브랜치로 구현했습니다. 초과 할당 없이 성공·품절 결과를 반환하고 처리량도 높은 조건부 UPDATE를 선택했습니다.
설계 이유와 트레이드오프
응답 시간 하나로 선택하지 않기
낙관적 락+재시도는 p95가 낮았지만 재시도 소진으로 끝나는 요청이 남았습니다. Atomic은 p95가 더 높아도 성공 100건·품절 900건을 반환하면서 높은 처리량을 보였고, 이후 개선의 기준으로 선택했습니다.
예약 판단을 트랜잭션 앞에서 실행
가용 수량 검사와 갱신을 하나의 UPDATE로 묶고 먼저 실행해 품절 요청의 불필요한 조회·저장을 줄였습니다. DB 내부 행 잠금은 여전히 존재하므로 같은 재고의 경합은 다음 단계에서 줄여야 했습니다.
낙관적 재시도에 backoff·jitter 추가 실험
후속 실험에서는 재시도 사이에 지수 backoff와 full jitter를 추가했습니다. 충돌 집중을 완화하려 했지만 5회 실험에서 재시도 소진이 184~222건 남아 품절과 구분되는 실패 유형을 함께 관찰했습니다.
V1 동시성 제어 전략 비교
전략
측정
RPS
p95
응답 결과
낙관적 락 · 재시도 없음
별도 1회
708.74
864.96ms
성공 100 / 충돌 749 / 품절 151
낙관적 락 + 재시도
5회 중앙값
2,237.88
113.33ms
성공 100 / 재시도 소진 202~226 / 나머지 품절
비관적 락
5회 중앙값
1,166.09
200.80ms
성공 100 / 품절 900
조건부 UPDATE · 선택
5회 중앙값
2,420.09
225.47ms
성공 100 / 품절 900
100 VUs · 1,000요청 · 단일 상품 · 재고 100개 · 요청당 1개. 반복 실행 사이 DB를 초기화했습니다. p95는 전체 요청 응답 시간이며 충돌·품절 응답도 포함합니다. VUs는 가상 사용자 수, RPS는 초당 요청 수, p95는 응답 시간의 95백분위 값입니다.
낙관적 backoff·jitter 추가 실험
측정
RPS
p95
응답 결과
5회 중앙값
2,109.78
116.98ms
성공 100 / 재시도 소진 184~222 / 나머지 품절
100 VUs · 1,000요청 · 재고 100개. V1 기본 비교 이후 진행한 별도 실험입니다.
VALIDATION
구현 결과와 테스트
선택한 Atomic 방식은 5회 실험 모두 성공 100건·품절 900건·초과 할당 0건을 유지했습니다. DB 예약 이후 주문·할당도 같은 트랜잭션으로 저장합니다.
V2 구현·실험 완료
DB 접근 최적화
조건부 UPDATE를 유지하면서 실행 계획과 성공 경로의 DB 작업량을 분석했습니다.
실험 브랜치experiment/v2-db-optimized
DB 접근 최적화 처리 흐름
SQL 관찰실행 계획 · 기존 인덱스
조회 축소ID · getReference
상태 반영저장 전 ALLOCATED
부하 비교RPS · p95 · 정합성
PROBLEM
문제
예약을 성공한 뒤 상품·창고·재고 전체 조회와 주문 상태 UPDATE가 추가로 발생해 트랜잭션 작업량을 늘렸습니다.
SOLUTION
해결
상품·창고는 getReferenceById로 연결하고 재고는 ID만 조회했습니다. 주문 상태를 최초 저장 전에 변경해 추가 UPDATE를 제거했습니다. 기존 복합 UNIQUE 인덱스와 실행 계획도 확인했습니다.
설계 이유와 트레이드오프
인덱스를 늘리기 전에 실제 병목 확인
실험의 작은 재고 테이블에서는 기존 복합 인덱스가 있어도 Seq Scan 비용이 더 낮았습니다. 같은 인덱스를 추가하는 대신 동일 재고 행의 경합과 예약 이후의 DB 작업을 줄이는 데 집중했습니다.
참조 연결에 필요한 정보만 조회
전체 엔티티의 값이 필요하지 않은 관계 연결에는 ID와 지연 참조를 사용했습니다. 불필요한 SELECT를 줄이는 대신 참조의 유효성과 지연 조회 발생 시점을 고려해야 합니다.
V1 Atomic → V2 DB 최적화
단계
RPS
p95
응답 결과
V1 Atomic
2,420.09
225.47ms
성공 100 / 품절 900
V2 DB 최적화
3,380.82
149.28ms
성공 100 / 품절 900
100 VUs · 1,000요청 · 재고 100개, 5회 실행의 지표별 중앙값.
VALIDATION
구현 결과와 테스트
100 VUs·1,000요청의 5회 중앙값에서 V1 Atomic 대비 RPS가 39.7% 증가하고 p95가 33.8% 감소했습니다. 성공 100건·품절 900건을 유지했습니다.
V3 구현·실험 완료
Redis 품절 처리
품절된 요청이 DB의 같은 재고 행에 도달하지 않도록 Redis Lua를 추가했습니다.
실험 브랜치experiment/v3-redis
Redis 품절 처리 처리 흐름
Lua 수량 검사품절이면 즉시 종료
Redis 선예약가용 수량 원자 차감
DB 트랜잭션Atomic · 주문 저장
할당 응답성공 결과 반환
Lua가 수량 부족을 반환하면 DB 접근 없이 품절로 응답합니다. 예약 후보만 DB 트랜잭션을 진행하며, DB 처리 실패 시 Redis 수량을 반환합니다.
PROBLEM
문제
DB 접근을 줄인 뒤에도 품절 요청이 모두 동일 재고 행에 도달해 경합과 커넥션 대기를 만들었습니다.
SOLUTION
해결
Lua로 가용 수량 검사와 차감을 원자적으로 처리하고 품절이면 즉시 응답했습니다. 예약 후보는 기존 DB 트랜잭션으로 처리하고, DB 실패 시 Redis 감소분을 반환하도록 구성했습니다.
설계 이유와 트레이드오프
Redis는 빠른 품절 판단, DB는 최종 재고 장부
DB의 조건부 UPDATE를 유지해 Redis가 최종 예약 판단을 대체하지 않도록 했습니다. 두 저장소의 원자적 커밋은 없으므로 프로세스 중단과 보상 실패는 V5의 불일치 탐지·복구 과제로 이어졌습니다.
V2 → V3 품절 요청 처리 개선
단계
RPS
p95
응답 결과
V2 DB 최적화
5,155.94
262.18ms
성공 100 / 품절 2,900
V3 Redis Lua
7,454.87
42.79ms
성공 100 / 품절 2,900
300 VUs · 3,000요청 · 재고 100개, 5회 중앙값. 품절 요청 비중이 높은 단일 상품 부하입니다.
VALIDATION
구현 결과와 테스트
300 VUs·3,000요청의 5회 실험에서 성공 100건·품절 2,900건·기타 오류 0건을 유지했습니다. 같은 조건의 V2 대비 RPS는 44.6% 증가하고 p95는 83.7% 줄었습니다.
V4 구현·실험 완료
Kafka·Outbox 확장
성능 개선 이후에는 할당 결과를 후속 작업으로 전달할 이벤트 발행 경로를 추가했습니다.
실험 브랜치experiment/v4-kafka-outbox
Kafka·Outbox 확장 처리 흐름
할당 트랜잭션재고 · 주문 · Outbox
Publisher미발행 이벤트 조회
Kafka 발행발행 상태 갱신
Consumer이벤트 수신
PROBLEM
문제
할당 저장과 Kafka 전송을 따로 처리하면 DB 커밋 이후 발행 실패로 이벤트가 누락될 수 있습니다.
SOLUTION
해결
할당 결과와 Outbox 이벤트를 같은 트랜잭션에 저장하고 Publisher가 커밋된 이벤트를 Kafka로 발행하도록 구현했습니다. Consumer를 연결해 향후 알림·배송 연동·분석 같은 후속 작업을 확장할 기반을 마련했습니다.
설계 이유와 트레이드오프
할당 요청과 후속 처리의 책임 분리
재고 할당은 동기 DB 트랜잭션으로 유지하고 후속 이벤트 발행을 분리했습니다. 모듈형 단일 애플리케이션에서도 연계 작업을 확장할 수 있지만, Outbox 저장 비용과 이벤트 지연을 관리해야 합니다.
at-least-once 전달을 전제로 설계
Kafka 전송 성공과 발행 상태 저장 사이에 중단되면 같은 이벤트가 다시 전달될 수 있습니다. 이 문제를 V5의 Consumer 멱등성과 재발행 실험으로 이어갔습니다.
V3 → V4 이벤트 기록 추가 비용
단계
RPS
p95
예약 결과
V3 Redis Lua
7,454.87
42.79ms
성공 100 / 품절 2,900
V4 Redis + Outbox
7,273.13
41.97ms
성공 100 / 품절 2,900
300 VUs · 3,000요청 · 재고 100개, 5회 중앙값. 전체 API 응답 성능을 측정했으며 Kafka 전체 처리량·발행 지연 수치는 아닙니다.
VALIDATION
구현 결과와 테스트
Kafka 중단 중에도 할당 API가 성공하고 미발행 이벤트가 DB에 남았습니다. Kafka 복구 후 발행 상태 전환과 수신을 점검했고, 부하 실험 후 생성된 이벤트 100건의 발행 완료를 확인했습니다.
V5 구현·실험 완료
멱등성·장애 복구
중복 요청·이벤트와 인프라 장애에서 데이터가 보존되고 재처리되는 흐름을 추가했습니다.
실험 브랜치experiment/v5-resilience-idempotency
멱등성·장애 복구 처리 흐름
중복 요청요청 키 · 기존 결과
중복 이벤트이벤트 ID · 소비 이력
발행 실패backoff · DEAD
복구 작업replay · 불일치 탐지
중복 요청 처리, 중복 수신 방지, 발행 실패 복구와 수량 불일치 탐지는 각각의 처리 경로입니다. 발행 실패는 backoff → DEAD 격리 → 수동 replay로 이어집니다.
PROBLEM
문제
API 재시도는 중복 주문을 만들 수 있고, 이벤트 재발행은 같은 후속 작업을 반복할 수 있습니다. DB 실패나 프로세스 중단은 Redis와 DB의 수량을 어긋나게 할 수 있습니다.
SOLUTION
해결
동일 요청 키에는 기존 주문을 반환하고 DB UNIQUE 제약으로 동시 중복 요청을 처리했습니다. Consumer는 이벤트 ID와 소비자 기준으로 최초 처리만 통과시킵니다. Publisher에는 지수 backoff·DEAD 격리·수동 replay를 추가하고 Redis/DB 가용 수량을 주기적으로 대조했습니다.
설계 이유와 트레이드오프
중복 검사와 실제 처리의 경계
DB 제약과 처리 이력으로 중복 요청·이벤트를 제어했습니다. Consumer의 실제 외부 연계 작업은 향후 추가할 계획이며, 외부 부작용에도 같은 이벤트 식별자를 전달해야 합니다.
실패 이벤트를 보존하고 명시적으로 재발행
반복 실패는 DB의 DEAD 상태에 격리하고 수동 replay로 다시 발행합니다. Kafka가 중단된 상황에서도 복구 대상을 남길 수 있지만, 운영 적용에는 관리 API 권한과 재발행 감사 이력이 필요합니다.
Redis와 DB 불일치 탐지를 먼저 구현
가용 수량을 주기적으로 대조하고 불일치·키 누락을 경고합니다. 진행 중인 요청의 선예약을 덮어쓰지 않도록 자동 보정은 하지 않고, 실험에서는 DB 기준으로 수동 복구한 뒤 일치 여부를 확인했습니다.
V5 실패·복구 시나리오
시나리오
결과
같은 요청 키 반복
기존 주문 반환 · 중복 주문/할당/이벤트 생성 방지
같은 이벤트 재발행
소비 이력 1건 · 중복 수신 건너뛰기
Kafka 반복 발행 실패
1·2·4·8초 backoff, 5번째 실패에 DEAD
DEAD 이벤트 replay
동일 이벤트 ID로 PENDING → PUBLISHED
Redis 수량 불일치
DB 100 / Redis 97 탐지 → 수동 복구 후 일치
API 중복 실험은 기능·정합성 점검이며 V3·V4 처리량 비교와 별도 조건입니다.
VALIDATION
구현 결과와 테스트
10 VUs·동일 요청 100건을 5회 반복해 총 500건 모두 성공했고 응답 주문 ID 불일치·기타 오류는 0건이었습니다. 각 실행 후 주문·할당·Outbox가 1건씩 생성되고 DB 예약 1개·Redis 가용 99개인 상태를 점검했습니다.
04 / RESULTS & TESTING
개발 결과와 테스트
WMS-Lite 개발 결과
항목
결과
V0 · 문제 재현
재고 100개에 840건 할당, 초과 할당 740건
V1 · 동시성 선택
조건부 UPDATE로 성공 100 / 품절 900 / 초과 할당 0
V2 · DB 개선
100 VUs 조건: RPS +39.7% · p95 −33.8%
V3 · 품절 응답
300 VUs 조건: V2 대비 RPS +44.6% · p95 −83.7%
V4 · 후속 연계 기반
할당·Outbox 함께 저장, Kafka 중단→복구 후 발행
V5 · 중복·복구
동일 요청 총 500건 오류 0, 중복 소비 방지·DEAD replay·수량 불일치 탐지
05 / REFLECTION
개발 회고와 개선 방향
회고
같은 재고 할당 API를 반복해서 개선하면서 문제에 따라 선택 기준이 달라졌습니다. V1에서는 초과 할당과 충돌 실패를, V2·V3에서는 트랜잭션 작업량과 품절 응답 비용을 줄였습니다. V4·V5에서는 성능만큼 이벤트 보존과 중복 처리, 복구 절차가 중요해졌습니다. 기술을 먼저 정하기보다 이전 단계의 병목과 실패 유형을 다음 실험의 출발점으로 삼았습니다.
NEXT / 향후 계획
향후 개선 방향
Consumer에 실제 후속 업무를 연결하고 외부 작업의 멱등성, 소비 실패 재시도와 DLQ를 확장할 계획입니다. 관리 API의 권한·감사 이력과 Redis/DB 수량의 안전한 재동기화 절차도 보완하려 합니다.