장바구니

거래 · 2026년 8월 31일 기준

  • 실측
  • 테스트
  • 병목은 프로시저가 아닌 커넥션 풀(리틀의 법칙 교차검증)
  • 프로시저 기반 가격 동기화
  • 기업별 계약가
  • 재고 연동
목차 5
  1. 개요
  2. 왜 이 설계가 필요했는가 (Problem)
  3. 테이블 설계
  4. 핵심 설계 결정 — 장바구니 갱신 프로시저
  5. 설계가 만든 결과 (Result)

개요

B2B 전용몰 장바구니 기능. 단순 상품 담기가 아닌, 기업별 계약가·재고·행사를 실시간 반영하는 동적 장바구니를 설계했다.

장바구니 진입 시 DB 프로시저가 모든 상품의 가격/재고를 일괄 갱신하여, 사용자에게 항상 최신 정보를 보여준다.


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

B2C 장바구니와 다른 B2B의 3가지 핵심 난제 — 단순 FK 참조 모델·애플리케이션 갱신으로는 해결 불가능했다.

이 3가지를 동시에 해결하기 위해 DB 프로시저 + 장바구니 스냅샷 컬럼 의 조합으로 결정했다.


테이블 설계

설계 배경 — 왜 5개 컬럼으로는 부족했나

초기 장바구니 테이블은 단순한 FK 참조 테이블이었다.

-- 초기 설계: 단순 FK 참조
장바구니(장바구니 ID, 기업, 사용자, 상품, 수량)

B2B 전용몰의 장바구니는 B2C와 다른 요구사항이 있었다.

B2B 요구사항 문제 해결 → 추가된 컬럼
기업마다 계약 가격이 다름 상품의 판매가를 그대로 쓸 수 없음 판매단가 · 할인가 · 기업 계약가
주문 시점의 가격을 확정해야 함 장바구니 → 주문 전환 시 가격이 변동될 수 있음 정가 · 공급가 · 부가세 · 소비자가
행사/쿠폰 할인액을 분리 추적 할인 출처별 금액을 구분해야 정산 가능 행사 할인 · 자동 쿠폰 · 수동 쿠폰 금액
공급사별 묶음배송 그룹핑 장바구니 화면에서 공급사별로 상품을 그룹핑 표시 공급사 · 공급사명 · 묶음배송 그룹
프로시저 계산 결과 저장 프로시저가 계산한 가격/수량/상태를 장바구니에 기록 스냅샷 컬럼 전체 (가격/상품/공급사)

이 요구사항들을 반영하면서 5개 컬럼짜리 참조 테이블이 가격 · 상품 · 공급사 스냅샷을 담는 수십 개 컬럼으로 확장되었다.

장바구니 최종 구조

분류 담는 것 역할
기본 식별 기업 · 사용자 · 상품 기업+사용자+상품 조합으로 장바구니 항목 식별
가격 스냅샷 판매단가 · 할인가 · 정가 · 공급가 · 부가세 프로시저가 갱신한 최신 가격 저장, 주문 생성 시 가격 확정용
상품 스냅샷 상품명 · 이미지 · 판매처 상품코드 상품명·이미지를 장바구니 자체에서 렌더링
공급사 스냅샷 공급사 · 공급사명 · 사업자번호 공급사별 그룹핑 표시에 활용
옵션 옵션 조합(JSON) · 옵션 · 옵션명 · 옵션 금액 JSON으로 다중 옵션 조합 저장, 정규화된 참조도 병행
할인/쿠폰 행사 할인 · 자동 쿠폰 · 수동 쿠폰 행사할인과 쿠폰을 분리하여 금액 추적
그룹핑 묶음배송 그룹 · 선택형 그룹 묶음배송 그룹, 선택형 그룹 지원
사용자 상호작용 선택 여부 · 수량 · 메모 선택 상태, 수량, 메모를 개별 관리

왜 스냅샷 컬럼이 필요했나

스냅샷 컬럼 없이도 기능 자체는 구현할 수 있다. 장바구니 조회 시 상품 · 기업별 상품(계약가) · 기업 테이블을 매번 JOIN하거나, 주문 생성 시 그 시점에 직접 상품/가격을 조회하면 된다.

그러나 스냅샷 컬럼과 프로시저는 세트다.

스냅샷 없이 (애플리케이션에서 직접) 스냅샷 + 프로시저 (현재 설계)
갱신 로직을 애플리케이션에서 구현
→ 상품당 3회 쿼리 (N+1 문제)
프로시저가 DB 내부에서 일괄 갱신
→ 1회 CALL로 완결
프로시저가 계산 결과를 저장할 곳이 없음
→ 임시 테이블 또는 OUT 파라미터로 반환
프로시저가 장바구니 스냅샷 컬럼에 직접 UPDATE
→ 별도 반환 로직 불필요
주문 생성 시 가격을 다시 조회해야 함
→ 장바구니와 주문 사이 가격 불일치 가능
주문 생성 시 장바구니 스냅샷을 그대로 사용
→ 장바구니에서 본 가격 = 주문 가격 보장

인덱스 설계

인덱스 기준 용도
기업 기업별 장바구니 조회
사용자 사용자별 장바구니 조회
상품 상품 기준 장바구니 존재 확인

핵심 설계 결정 — 장바구니 갱신 프로시저

검토했던 대안들 (Analyze)

“장바구니 진입 시 모든 상품의 최신 가격·재고 동기화”를 위한 5가지 방법을 비교했다.

대안 장점 기각 사유
1. 매번 조회 시 JOIN (실시간 계산) 스냅샷 불필요, 항상 최신 장바구니 조회 빈도 높음 → 매번 상품 · 기업별 상품 JOIN 수행 비용. 주문 생성 시점과 가격 불일치 위험 여전히 존재
2. Redis 가격 캐싱 (TTL 5분) 빠른 조회, 캐시 적중률 높음 가격 변동 시점과 캐시 만료 사이 정합성 깨짐. 기업별 계약가 캐시 키 폭증(M기업 × N상품). Redis SPOF
3. 비동기 스케줄러 갱신 (5분 주기) 장바구니 조회 부하 0 사용자가 본 시점과 갱신 시점 차이 → 가격 불일치 동일 발생. “왜 어떤 사람만 이전 가격?” 디버깅 난해
4. 애플리케이션 + 배치 로딩 (Batch Fetch) JPA 유지하면서 N+1 회피 여전히 외부 호출 발생. 가격 계산·재고 조정 로직이 Java에 분산 → 주문 생성 프로시저와 로직 중복
5. DB 프로시저 + 장바구니 스냅샷 1회 RT, 가격·재고·계약가 DB 내부 완결, 주문 생성 시 스냅샷 재사용 DB 종속성, 프로시저 디버깅 비용 (수용 가능)

비교: 애플리케이션 레벨 vs DB 프로시저

❌ 애플리케이션 레벨로 구현했다면

장바구니 아이템 N개를 순회하며 상품당 3회 쿼리(상품 조회 → 계약가 조회 → UPDATE)가 발생한다.

// 애플리케이션 레벨로 갱신한다면 (가정한 의사코드)
장바구니 = 사용자 · 기업의 장바구니 조회  // 1회
for 항목 in 장바구니:  // N개 순회
    상품 = 상품 최신 정보 조회(항목)  // N회
    if 상품 없음 또는 판매중지: 항목 삭제 표시, 다음 항목으로
    계약가 = 기업별 계약가 조회(항목, 기업)  // N회
    할인가 = 할인 계산(정가, 할인율)  // 메모리에서 계산
    수량 = 재고 · 최대 주문수량에 맞춰 조정
    장바구니 스냅샷 갱신(할인가, 계약가, 수량)  // N회
// 총 쿼리: 1 + 3N (상품 20개 → 61회)

✅ DB 프로시저로 구현한 결과

단 1회 CALL로 DB 내부에서 커서 기반 일괄 처리한다.

-- 장바구니 갱신 프로시저 (요지)
FOR 항목 IN (사용자 · 기업의 장바구니) LOOP  -- 커서로 DB 안에서 순회
    상품 정보 조회 · 판매 가능 검증  -- DB 내부 호출, 네트워크 왕복 없음
    기업별 계약가 조회
    할인가 = FLOOR(정가 × (1 − 할인율 / 100))
    수량 = 재고 · 최대 주문수량에 맞춰 조정
    UPDATE 장바구니 SET 판매단가 · 할인가 · 계약가 · 수량 …
END LOOP

성능 비교 (장바구니 상품 20개 기준)

항목 애플리케이션 레벨 DB 프로시저 개선 효과 측정 방법
네트워크 라운드트립 30회 (IN 절 배치)
167회 (상품당 조회)
4회 배치 대비 26회 절감 ✅ 실측 완료 — MySQL 전역 카운터 델타
DB 커넥션 점유 쿼리마다 획득/반환 반복 단일 커넥션으로 완결 커넥션 풀 경합 제거 ✅ 실측 완료 — 부하 시 활성 20/20 · 대기 75
SQL 파싱 오버헤드 61회 개별 파싱 프로시저 1회 (사전 컴파일) 파싱 비용 제거 ⚠️ 미측정 — 파싱 비용만 분리해 재지 않았다
응답 시간 반복 RT 누적 p95 78ms (무경합) / 600ms (동시 100 VU) — ✅ 실측 완료 (k6, 아래 Result)
상품 50개 시 왕복은 라인 수와 무관한 상수
(IN 절로 묶으면 30회 고정)
4회 고정 격차는 일정하게 유지 ✅ 실측 — 제대로 짜면 앱도 N+1 이 아니다

프로시저 동작 흐름

프로시저 동작 흐름 다이어그램프로시저 동작 흐름 다이어그램

프로시저 핵심 로직

단계 처리 내용 참조
1. 상품 검증 삭제·판매중지 상품 → 장바구니에서 삭제 표시 상품
2. 계약가 조회 기업별 계약 판매단가 조회 기업별 상품(계약가)
3. 할인 계산 FLOOR(정가 × (1 − 할인율 / 100))
할인율 범위 0~100 보정
—
4. 재고 조정 재고관리 상품: 재고 초과 시 재고 수량으로 조정
최대 주문수량 초과 시 조정
상품
5. 결과 반환 갱신 · 제거 · 수량 조정 건수 —

설계가 만든 결과 (Result)

측정 조건

항목 값
대상 장바구니 조회 API — 요청마다 장바구니 갱신 프로시저 실행
환경 로컬 단일 인스턴스 · MySQL 8.4 컨테이너 · HikariCP 최대 20
데이터 사용자 120명이 각자 장바구니 20개
부하 2 VU 워밍업 → 10 → 50 → 100 (2분 45초), 생각시간 없음
계측 Micrometer → Prometheus (SLO 버킷 100ms~3s) + Grafana

실측 결과

지표 실측 조건
응답시간 p95 78 ms 무경합 (2 VU), 장바구니 20개
응답시간 p50 57 ms 동일
p95 600 ms 동시 100 VU
p99 943 ms 동시 100 VU
처리량 146 req/s 동시 100 VU
실패율 0 % 24,043 요청

장바구니 20개의 가격·기업계약가·재고를 1회 CALL 로 갱신하는 데 p95 78ms.

병목은 프로시저가 아니라 커넥션 풀이었다

부하 구간의 서버측 지표가 병목 위치를 특정했다.

지표 값 해석
HikariCP 활성 20 / 20 풀 포화
HikariCP 대기 75 동시 100 요청 중 75개가 커넥션 대기
MySQL CPU 1.3 % DB 는 놀고 있음
행 잠금 대기 증가 53건 잠금 경합 아님

트랙 순도 가드 검증

재측정 — 풀 크기만 바꿔 A/B (2026-08-30)

지표 풀 5 풀 20 배수
처리량 115.1 req/s 280.5 req/s 2.44배
총 요청 18,989 46,285 —
p50 191.6 ms 83.6 ms 2.29배
p95 813.3 ms 291.3 ms 2.79배
p99 1,129.3 ms 429.0 ms 2.63배
실패율 0.00 % 0.00 % —

이 재측정이 말하지 않는 것

이 측정이 말하지 않는 것