주문/결제

거래 · 2026년 9월 6일 기준

  • 실측
  • 테스트
  • SO/PO 분리
  • 프로시저 기반 주문 생성
  • PG 결제
  • 여신/예산
목차 5
  1. 개요
  2. 왜 이 설계가 필요했는가 (Problem)
  3. 테이블 설계 — SO/PO 이중 구조
  4. 핵심 설계 결정
  5. 설계가 만든 결과 (Result)

개요

B2B 구매 전체 프로세스를 담당하는 핵심 기능. 장바구니 → 주문(SO) → 공급사별 발주(PO) → 결제(PG/여신) → 배송까지의 흐름을 SO/PO 이중 구조로 설계했다.

하나의 주문에 여러 공급사 상품이 포함될 수 있으므로, 공급사별 배송·정산을 독립적으로 관리한다.

SO/PO 이중 구조 결제 이중 경로 13개 주문 상태 다중행 INSERT→1 CALL 동시 이중차감 원천 차단
공급사별 독립 배송·정산 PG + 여신·예산 SO 13개 + 발주 라인 독립 상태 주문 생성(SO)·발주 생성(PO) 프로시저 여신·재고 FOR UPDATE 행 잠금

왜 이 설계가 필요했는가 (Problem)

B2B 전용몰에서 마주친 3가지 핵심 난제 — 단일 SO 모델·애플리케이션 레벨 트랜잭션으로는 해결 불가능했다.

이 3가지를 동시에 해결하기 위해 SO/PO 이중 구조 + DB 프로시저 + SELECT FOR UPDATE 행 잠금 의 조합으로 결정했다.


테이블 설계 — SO/PO 이중 구조

검토했던 대안들 (Analyze)

대안 장점 단점 선택
1. 단일 SO + 공급사 항목 JSON 컬럼 스키마 단순, 단일 row로 주문 파악 공급사별 상태 변경 시 전체 row UPDATE → 동시성 충돌. JSON 인덱스 활용 한계로 공급사별 조회 비효율 ❌ 기각
2. SO + 공급사별 상태 별도 테이블 SO 변경 없이 공급사별 상태 추적 SO 본체와 상태 테이블 사이 정합성 보장 어려움. 출고/취소 트랜잭션 분리 어려움 ❌ 기각
3. SO + PO 이중 구조 공급사별 상태머신 독립, FK로 정합성 자동 보장, PO 단위 정산 지급 가능 테이블 수 증가, 조인 복잡도 ↑ ✅ 선택

ERD (핵심 관계)

ERD (핵심 관계) 다이어그램ERD (핵심 관계) 다이어그램

테이블별 설계 의도

테이블 설계 의도
고객 주문 (SO 헤더) 내부 PK 와 외부 노출용 주문번호를 분리 · 라인당 5개 금액을 헤더 11개 집계 금액으로 롤업해 정산 근거를 추적 · 주문 시점의 기업/주문자 스냅샷 · 결제수단(후불 여신 / PG) 분기 · 사내 결재 연동
주문 상품 (SO 라인) 상품명·가격·이미지 스냅샷 → 이후 가격 변경에 영향 없음 · 선택한 옵션 조합과 추가금액 · 라인별 할인/쿠폰/포인트 개별 추적 · 어떤 장바구니 항목에서 전환됐는지 추적
공급사 발주 (PO) SO 1건 → 공급사별 PO N건 자동 분리 — 결제 확정 후 발주 생성 프로시저가 주문 라인의 공급사를 기준으로 만든다(같은 상품은 발주 라인 1건으로 집계) · 공급사 스냅샷 · SO 와 독립된 상태: 신규주문 → 접수 → 배송중 → 배송완료
발주 상품 (PO 라인) 직배송/택배 · 고객 직배송/허브 경유 구분 · 취소 귀책 2트랙 — 구매자 취소 수량과 공급사 취소 수량을 따로 관리해 정산 분쟁 방지 · 상품 단위 배송 상태
결제 PG 결제 + 여신/예산 결제 이중 경로 · PG 결제 식별키 · 카드·가상계좌·간편결제별 기록 · 부분취소와 환불 가능 금액 추적
배송지 기업 공용 / 개인 배송지 구분 · 수령 방법(경비실, 문앞 등) · 출입 방법(공동현관 등)

주문 상태 머신 (13개):

테이블별 설계 의도 다이어그램테이블별 설계 의도 다이어그램

핵심 설계 결정

1. 주문 생성 프로시저 — 트랜잭션 정합성 + 동시성 제어

검토했던 6가지 대안 (Analyze)

“여러 테이블 동시 INSERT + 여신 차감”의 트랜잭션 정합성과 동시성을 보장할 6가지 방법을 비교했다.

대안 장점 기각 사유
1. JPA + @Transactional (순수) 구현 단순, 모든 로직 Java 코드 상품·공급사 수에 비례한 다수 RT, 조회→차감 사이 race condition (이중 차감 위험)
2. JPA + 비관적 락(@Lock(PESSIMISTIC_WRITE)) + JDBC 배치 INSERT 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 이상이면 차단 성공(프로시저)

프로시저 처리 순서:

성능 비교 (주문 1건 · 장바구니 20라인 기준) 다이어그램성능 비교 (주문 1건 · 장바구니 20라인 기준) 다이어그램

2. 스냅샷 전략 — 3개 원본 → 3개 대상 복사

3개 원본(상품 · 사용자 · 기업)에서 3개 대상(주문 헤더 · 주문 라인 · 발주)으로 주문 시점 데이터를 스냅샷 복사한다.

이후 상품 가격·사용자 정보가 변경되어도 기존 주문에 영향 0건 — 가격 변동에 의한 정산 분쟁을 원천 차단한다.

스냅샷 대상 원본 복사 위치
상품명, 가격, 이미지 상품 주문 라인
주문자, 기업 정보 사용자 · 기업 주문 헤더
공급사 정보 기업 발주

3. 이중 상태 머신 — SO 13개 + 발주 라인 독립 상태

레벨 관리 단위 상태 예시
SO 레벨 (주문 헤더) 주문 전체 주문요청 → 신규주문 → 배송완료
발주 라인 레벨 개별 상품 배송 대기 → 배송중 → 배송완료

SO 전체가 “배송중”이더라도, 개별 발주 라인은 공급사별로 다른 배송 상태를 가질 수 있다.

SO 13개 상태 전이 + 발주 라인 별도 배송 상태 → 공급사별 독립 추적으로 복합 주문의 부분 배송·부분 취소를 정확히 표현한다.

4. 결제 이중 경로

4. 결제 이중 경로 다이어그램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 승격 세 가지다. 메커니즘이 서로 달라 하나만 고치면 개선이 안 나거나 교착이 다른 자원으로 옮겨간다.

부수 확인 — §6 실패·보상 처리가 실증되었다

이 측정이 말하지 않는 것