시퀀스 다이어그램 1 배치 성공 경로
--------------------------------------------------------------------------------
Client PaymentController Kafka KafkaProductUpdateListener ProductService PaymentService
| POST /b1/payment/request-batch | | | |
|----------------------------------->| want = toInfos(user, items) | | |
| | existingRidForAny(user, want) -> empty | | |
| | rid = newRid(user) | | |
| | sigKey = buildSignatureKey(user, want) | | |
| | registerSignatureIfAbsent(sigKey, rid) -> ok | | |
| | merged = mergeByOption(want) 동일 옵션 서버 병합 | | |
| | for it in merged: addReservation(rid, it) 번들 구성 | | |
| | bindRidSignature(rid, sigKey) 정리 경로용 바인딩 | | |
| | send PRODUCT_UPDATE_BATCH_TOPIC(merged) ---------------------------------------->| | |
|<----------------------------------| ApiResponse.ok(rid, eta, items) | | |
| | | @Transactional begin | |
| | | for it in merged: | processPayment... -------------->|
| | | | afterCommit 예약 보강 등록 |
| | | commit | |
| | | afterCommit: | addReservation(rid, it) -------->| 중복이면 no-op
| | | | |
--------------------------------------------------------------------------------
정합성 검증 포인트
- 병합으로 동일 옵션은 단 1회 차감
- afterCommit에서만 예약 보강과 SSE 발행
- 컨트롤러 선점이 이미 있어도 addReservation는 멱등 no-op
시퀀스 다이어그램 2 옵션 미존재로 실패, 전체 롤백, 즉시 정리
--------------------------------------------------------------------------------
Client PaymentController Kafka KafkaProductUpdateListener ProductService PaymentService
| POST /b1/payment/request-batch | | | |
|----------------------------------->| 병합, 시그니처 선점, addReservation, 바인딩, 배치 발행 완료 | | |
| | | @Transactional begin | |
| | | afterCompletion 등록 | |
| | | try { | |
| | | for it in merged: | processPayment... -------------->|
| | | ... 마지막 it에서 옵션 미존재 예외 | throw BasiliumCustomException
| | | } catch (RuntimeException ex) { | |
| | | cancelAllNoRestore(rid) -------------------->| 키와 타이머 즉시 제거
| | | throw ex 롤백 보장 | |
| | | TX rollback | |
| | | afterCompletion(rolled_back): | |
| | | cancelAllNoRestore(rid) -------------------->| 2차 방어 idempotent
| | | no afterCommit SSE 미발행 | |
| | | 레코드 재시도 정책 | |
--------------------------------------------------------------------------------
정합성 검증 포인트
- DB 차감은 트랜잭션 롤백으로 전부 되돌아감
- SSE는 afterCommit에서만 발행하므로 실패 경로에서 절대 발행되지 않음
- 즉시 정리로 stale 키 레이스 윈도우 최소화
- afterCompletion에서 중복 제거 시도도 안전한 no-op
시퀀스 다이어그램 3 동일 옵션 중복 입력도 1회 차감과 정확 복구
--------------------------------------------------------------------------------
Client PaymentController Kafka KafkaProductUpdateListener ProductService PaymentService
| items: (1,M,BLACK,3) x2, (2,M,BLACK,3) |
|----------------------------------->| merged: (1,M,BLACK,6), (2,M,BLACK,3) |
| | addReservation(rid, (1,M,BLACK,6)), addReservation(rid, (2,M,BLACK,3)) |
| | send batch((1,M,BLACK,6), (2,M,BLACK,3)) ----------------------------------------->|
| | | 트랜잭션에서 각 항목 1회 차감
| 실패 시 | | catch 즉시 cancelAllNoRestore, rollback
| finalizeByRid(false) 또는 만료 시 | | | restoreProductQuantity(1,M,BLACK,6) 정확 복구 1회
| | | | restoreProductQuantity(2,M,BLACK,3) 정확 복구 1회
| | | | 인덱스와 시그니처 제거
--------------------------------------------------------------------------------
정합성 검증 포인트
- 동일 옵션은 병합되어 번들에 단일 CompositeKey만 존재
- 실패 확정이나 만료에서 복구는 병합된 총량 기준으로 정확히 1회
- 실패 트랜잭션 경로에서는 DB가 롤백되므로 복구 자체가 호출되지 않음
🚀 배치 예약 중 옵션 미존재와 동일 옵션 중복으로 인한 재고 중복 차감, 복구 불일치 이슈에 대한 최종 기술 문서
문제상황
문제 원인과 재현 시나리오
핵심 설계 변경 요약
컴포넌트와 데이터 구조
불변 조건
컨트롤러 설계 세부
리스너 설계 세부
PaymentService 설계 세부
ProductService 설계 세부
성능과 동시성
로깅과 관측성
품질 규약 준수
검증 시나리오
리스크와 완화
결론