Java 코드 유지하며 동시성·RT 동시 개선 — 락으로 이중 차감 차단, 배치로 INSERT 왕복 축소
배치로도 RT 잔존(다중 statement·flush), 락 보유 구간이 앱↔DB 왕복만큼 길어져 동시 처리량 저하. 로직이 앱·DB로 분산
3. Saga 패턴 + 이벤트
마이크로서비스 확장성, 부분 실패 복구
결제 실패 시 보상 트랜잭션 N개 작성 → 운영 복잡도 ↑↑. 현 인력으로 운영 부담
4. Outbox 패턴 + 비동기 처리
최종 일관성 보장, 메시지 신뢰성
주문번호를 사용자에게 동기 응답해야 함 → 비동기 부적합
5. Redis 분산 락 + 애플리케이션
JPA 유지하면서 동시성 제어
락 획득/해제 비용, Redis 단일 SPOF, 락 해제 실패 시 데드락 위험
6. DB 프로시저 + SELECT FOR UPDATE
1회 RT, 행 잠금으로 race condition 원천 차단, 단일 DB 트랜잭션
DB 종속성, 디버깅 난이도 ↑ (Flyway 형상관리로 완화 — 아래 참고)
비교: 애플리케이션 레벨 vs DB 프로시저
❌ 애플리케이션 레벨로 구현했다면
주문 생성 시 상품 N개, 공급사 M개 기준으로 다수의 독립 쿼리가 발생한다.
// 애플리케이션 레벨 구현(가정) — 라인마다 쿼리를 반복하면장바구니 조회 // 1회주문 헤더 저장 + 주문번호 채번 // 2회for 장바구니 라인: 주문 라인 저장 // N회for 공급사: 발주 저장, for 라인: 발주 라인 저장 // M + N회결제 저장 // 1회if 여신 결제: 여신 조회 + 차감 // 2~3회for 장바구니 라인: 장바구니 삭제 // N회// 골격만도 3N + 2M + 6 ≈ 42회 (상품 10 · 공급사 3). 라인별 가격 재검증 · 재고 잠금까지 넣으면 더 많다
✅ DB 프로시저로 구현한 결과
주문 생성 프로시저 1회 CALL로 SO측 다중행(헤더 + 라인 N + 배송지 + 이력 + 장바구니 정리)을 원자적으로 처리한다. 여신 차감·PO·결제는 포함하지 않는다(앱 계층·발주 생성 프로시저 담당).
-- 주문 생성 프로시저 — SO 쪽 다중행 처리만 1 CALL 로BEGIN -- 1. 정합성 검증 (배송지 · 결제수단 · 주문불가 상품) -- 2. 주문 헤더 INSERT (주문요청대기 재사용 또는 신규) + 주문번호 채번 -- 3. 배송지 스냅샷 INSERT -- 4. 커서로 장바구니 라인 순회: 상품 SELECT ... FOR UPDATE (재고) → 카트가 vs 현재가 재검증 → 주문 라인 INSERT → 재고 차감 -- 5. 헤더 금액 집계 + 이력 + (결재선) + 장바구니 정리END;-- 여신 차감 · PO · 결제는 하지 않는다 (Spring @Transactional 서비스가 담당)
성능 비교 (주문 1건 · 장바구니 20라인 기준)
항목
애플리케이션 레벨
DB 프로시저
개선 효과
측정 방법
다중행 INSERT 라운드트립
30회 (IN 절로 묶은 배치 구현)
11회
왕복 19회 절감
✅실측 완료 — MySQL 전역 카운터 델타 (4회 반복 동일값)
동시성 제어 (여신·재고)
락 없으면 조회→차감 race → 이중 차감 위험
앱: 여신 행 FOR UPDATE 직렬화 + 차감 직전 재조회 저지선 프로시저: 재고 행 FOR UPDATE
동시성 안전 (2지점 행잠금)
✅실측 완료 — 동시 120건에서 초과차감 0건 (아래 Result)
DB 커넥션 점유
트랜잭션 동안 왕복마다 점유 시간 누적
한 번의 CALL 로 완결
주문당 점유 구간 단축
⚠️ 동시 120건 부하 중 커넥션 풀 지표 — 활성 20/20 · 대기 75 (풀 최대 20). 점유는 줄었지만 이 부하에서는 풀이 포화했다
응답 시간 (지연 1ms 주입)
403 ms
276 ms
1.46배 (절대 −127ms)
✅실측 완료 — TCP 프록시로 0·1·2·5ms 주입 후 곡선 확보
상품 30개·공급사 5개 시
왕복은 라인 수와 무관하게 상수 (IN 절로 묶으면 30회 고정)
11회 고정
상품 수가 늘어도 격차는 일정
✅ 실측 — 제대로 짜면 앱도 N+1 이 아니다
// k6 — 동시 주문 부하 + 이중차감 검증 (요지)import http from 'k6/http';import { check } from 'k6';export const options = { scenarios: { burst: { executor: 'shared-iterations', vus: 120, iterations: 120, maxDuration: '120s' } } };export default function () { const res = http.post(__ENV.URL, PAYLOAD, { headers: H }); check(res, { created: (r) => r.status === 200 });}// 종료 후: SELECT 여신잔액 — 음수면 이중차감(락 미적용), 0 이상이면 차단 성공(프로시저)
프로시저 처리 순서:
2. 스냅샷 전략 — 3개 원본 → 3개 대상 복사
3개 원본(상품 · 사용자 · 기업)에서 3개 대상(주문 헤더 · 주문 라인 · 발주)으로 주문 시점 데이터를 스냅샷 복사한다.
이후 상품 가격·사용자 정보가 변경되어도 기존 주문에 영향 0건 — 가격 변동에 의한 정산 분쟁을 원천 차단한다.
스냅샷 대상
원본
복사 위치
상품명, 가격, 이미지
상품
주문 라인
주문자, 기업 정보
사용자 · 기업
주문 헤더
공급사 정보
기업
발주
3. 이중 상태 머신 — SO 13개 + 발주 라인 독립 상태
레벨
관리 단위
상태 예시
SO 레벨 (주문 헤더)
주문 전체
주문요청 → 신규주문 → 배송완료
발주 라인 레벨
개별 상품 배송
대기 → 배송중 → 배송완료
SO 전체가 “배송중”이더라도, 개별 발주 라인은 공급사별로 다른 배송 상태를 가질 수 있다.
SO 13개 상태 전이 + 발주 라인 별도 배송 상태 → 공급사별 독립 추적으로 복합 주문의 부분 배송·부분 취소를 정확히 표현한다.
4. 결제 이중 경로
PG 결제: PG 결제창 → 승인 → 결제 키로 서버 확인(결제 승인 API) → 발주 생성
여신/예산: 앱 계층에서 기업 여신 행 FOR UPDATE 잠금 + 차감 → 발주 생성 → 즉시 완료
여신/예산 4단계 출처(사용자·조직·계정조직 예산 + 기업 여신): 자동 폴백이 아니라 권한 위계 순으로 제시 → 사용자 선택. 예산사용 기업은 예산 필수, 비예산 기업은 기업 여신 직접 차감
5. 결제 멱등성 — 중복 승인 차단
6. 실패·보상 처리
7. 여신 동시성 — 단일 직렬화 지점
설계가 만든 결과 (Result)
실험 설계
항목
값
여신 한도
1,000,000 원
건당 결제액
198,000 원 (장바구니 20개 × 계약가 9,900)
동시 요청
120건 — 램프 없이 동시 발사
기대값
floor(1,000,000 ÷ 198,000) = 5건만 성공
데이터
사용자 120명이 각자 장바구니 — 단, 여신은 기업 단위로 공유
판정
k6 응답이 아니라 DB 원장(기업 여신 테이블)으로 검증
결과 — 초과차감 0건
지표
실측
기대
주문 성공
5 건
5 건 ✅
여신 사용액
990,000 원
5 × 198,000 ✅
최종 잔액
10,000 원
≥ 0 ✅
여신부족 거절
101 건
가드가 차단
5xx
0 건
—
재측정 — 매퍼 가드 제거 후에도 초과차감 0건 (2026-08-30)
지표
구 경로 (매퍼 가드 있음)
현 경로 (2026-08-30 재측정)
주문 성공
5 건
5 건✅ (기대 5)
여신 사용액
990,000 원
990,000 원✅ (5 × 198,000)
최종 잔액
10,000 원
10,000 원✅ (기대 10,000)
여신부족 거절
101 건
72 건
기타 거절
14 건 (로컬 패치 적용분)
43 건 (리포 상태)
└ 그중 실제 교착 (앱 로그 기준)
10 건
38 건
5xx
0 건
0 건
부하 중 발견한 실제 결함 — 동시 주문 데드락
핵심만 요약하면 — 이 부하에서 난 교착의 원인은 ① 상품 ↔ 채번 카운터 잠금 순서 역전, ② 주문번호 임시값 ‘TEMP’ 의 유니크 인덱스 충돌, ③ 채번 루틴 내부의 S→X 승격 세 가지다. 메커니즘이 서로 달라 하나만 고치면 개선이 안 나거나 교착이 다른 자원으로 옮겨간다.