반품/교환(클레임)

거래 · 2026년 8월 31일 기준

  • 테스트
  • 실측
  • 헤더-라인-수거 3계층 상태머신
  • 부분 클레임 금액 배분(잔여 비례+마지막 흡수)
  • 마이너스 PO 정산
  • 포인트 FIFO 환수
목차 6
  1. 개요
  2. 왜 이 설계가 필요했는가 (Problem)
  3. 테이블 설계 — 헤더-라인-수거 3계층
  4. 상태 머신 — 유형별 (취소 / 반품 / 교환)
  5. 핵심 설계 결정
  6. 설계가 만든 결과 (Result)

개요

B2B 전용몰의 취소·반품·교환 전체 프로세스를 담당하는 기능. 주문(SO)·발주(PO)와 맞물려 구매자 신청 → 공급사 처리(승인·수거·검수·환불/재배송) → 관리자 감독의 3주체 워크플로우를 단일 도메인으로 설계했다.

배송 전 취소, 배송 후 반품(환불), 교환(맞배송)을 하나의 클레임 모델로 통합하되, 물리 처리(수거·검수·재배송)는 라인·박스 단위로 독립 추적한다.

헤더-라인-수거 3계층 3개 유형 / 32개 상태 마이너스 PO 정산 포인트 FIFO 환수
신청 · 실무 처리 · 물류 단위를 분리 취소 5 · 반품 13 · 교환 14 원 PO 불변 + 음수 PO 생성 만료 가드 + FOR UPDATE

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

B2B 클레임은 단순 환불이 아니다 — 부분 출고·여러 공급사·여신/포인트 결제·정산 추적이 얽혀, 단일 테이블·단순 환불 로직으로는 정합성이 깨진다.

이 난제들을 헤더-라인-수거 3계층 + 마이너스 PO + 잔여 기준 금액 배분 + FIFO 포인트 환수의 조합으로 해결했다.


테이블 설계 — 헤더-라인-수거 3계층

ERD (핵심 관계)

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

테이블 구성

구성 역할 설계 포인트
클레임 헤더 클레임 1건 (취소/반품/교환 = 주문 단위) 유형·대표상태·금액 집계 캐시·합포장 요청/승인 표시
클레임 라인 SKU·분할 물리 처리 단위 수거·검수·반송·재배송이 라인별 독립. 부모 라인 자기참조로 분할
수거 운송장·박스 단위 헤더 원배송 1:1 또는 합포장 N:1. DB 생성 컬럼으로 중복 수거 차단
환불 내역 PG/여신/예산/포인트 복원 수단별 행 분리. 환불계좌번호·예금주명 AES-256 암호화
상태 이력 상태 전이 이력 (이전→다음) 3개 채널이 공유하도록 공통 모듈로 통합

클레임 라인 — 수량과 금액 5차원 (정산의 핵심)

분류 담는 것 설계 의도
분할 구조 라인 ID · 부모 라인(자기참조) · 분할 원본 표시 검수 시 합격/불합격으로 라인을 쪼갤 때 원본을 아카이브(분할 원본 표시)하고 자식 생성
사유/귀책 사유(고객사귀책/공급사귀책 2그룹) · 귀책 · 최종 귀책 검수 후 귀책 재판정을 별도 값으로 추적
수량 · 금액 5차원 수량 · 금액 · 부가세 · 쿠폰 · 구매포인트 · 지급포인트 쿠폰·포인트는 “원주문 라인 값 × 클레임 수량 / 원주문 수량” 비례 배분. 헤더 합계가 이를 SUM
수거/검수/반송/재배송 수거 송장 · 검수 결과 · 반송 송장 · 재배송 송장 · 교환 발주 참조 물리 처리 4단계를 라인 단위로 송장·상태 관리. 재배송은 교환 전용

상태 머신 — 유형별 (취소 / 반품 / 교환)

① 취소 — 물리 흐름 없음

상태 머신 — 유형별 (취소 / 반품 / 교환) 다이어그램상태 머신 — 유형별 (취소 / 반품 / 교환) 다이어그램

② 반품 — 공급사 주도, 환불로 종결

상태 머신 — 유형별 (취소 / 반품 / 교환) 다이어그램상태 머신 — 유형별 (취소 / 반품 / 교환) 다이어그램

③ 교환 — 수거 후 재배송으로 종결

상태 머신 — 유형별 (취소 / 반품 / 교환) 다이어그램상태 머신 — 유형별 (취소 / 반품 / 교환) 다이어그램

핵심 설계 결정

1. 부분 클레임 금액 배분 — 잔여 비례 + 마지막 흡수

검토한 대안 (Analyze)

대안 장점 단점 선택
1. 단가 × 클레임수량 계산 단순·직관적 할인·쿠폰·포인트가 섞인 실결제액과 불일치. 매 클레임 반올림 오차 누적 ❌
2. 매번 (실결제액 × 비율) 재계산 항상 원본 기준이라 계산 근거 단순 라운딩 차이 누적 → 전량 반품 시 원금 불일치, 다음 클레임 잔여가 음수가 되는 버그 ❌
3. 잔여 비례 + 10원 내림 + 마지막 흡수 + 저장값 재사용 Σ(배분액) = 원결제액 정확 일치 보장. 부분 클레임의 차이(최대 10원)는 누적되지 않고 마지막 클레임에서 정산 ① 회차별 금액이 순서 의존 — 같은 수량도 회차마다 환불액이 다를 수 있고 마지막 회차가 끝수 흡수(CS 안내 필요) ② “기청구 수량/금액” 집계 정확성에 전체가 의존 — 활성 클레임만·분할 라인 이중집계 방지가 전제 ③ 배분값 영구 저장 관리 부담 — 저장값·재계산값 드리프트 금지 규칙 필요 ✅ 선택

판단 기준은 오차의 크기가 아니라 소멸 여부 — 대안 1·2의 오차는 합계가 원결제액과 영구히 불일치하는 정산 오차이고, 채택안의 최대 10원은 중간 회차의 일시적 차이로 전량 클레임 완료 시 합계가 정확히 일치한다(마지막 흡수로 정산).

잔여금액 = 총금액 − 기청구금액
잔여수량 = 총수량 − 기청구수량
if 잔여금액 ≤ 0 또는 잔여수량 ≤ 0: return 0  // 잔여 없음
if 요청수량 ≥ 잔여수량: return 잔여금액  // 마지막 클레임 = 잔여 전부 흡수
배분 = 잔여금액 × 요청수량 / 잔여수량  // 정수(long) 연산으로 overflow 방지
return 10원 단위 내림(배분)  // 부분 클레임 = 10원 단위 내림

2. 마이너스 PO — 원 PO 불변 + 음수 발주로 정산 순액 자동화

검토한 대안 (Analyze)

대안 장점 단점 선택
1. 원 PO 직접 차감(UPDATE) 테이블 단순 발주/매출 이력 파괴, 부분취소 누적 시 원본 복원 불가, 정산 추적 불가 ❌
2. 원 PO soft-delete 후 재생성 이력 일부 보존 발주 연속성·PG/배송 참조 단절 ❌
3. 마이너스(-) PO 별도 생성 원 PO 불변, 순액 자동 산출, 정산/매출 PO 기준 일관, 양방향 참조로 추적 행 증가, 부호 정합 관리 필요 ✅ 선택
// 헤더·라인의 모든 금액 필드를 음수로 통일 → 정산 합산이 깨지지 않음
마이너스 PO.주문금액 = −클레임 총액
마이너스 PO 라인.수량 = −클레임 수량
마이너스 PO 라인.공급가 = −공급가,  부가세 = −부가세
마이너스 PO 라인.결제금액 = −클레임 금액  // 결제금액은 VAT 포함 → 부가세를 따로 더하지 않음(이중계산 방지)
클레임.마이너스 PO = 마이너스 PO  // 클레임 ↔ 마이너스 PO 양방향 참조

3. 포인트 FIFO 역추적 환수 + 만료 가드

// 사용이력 버킷을 최신순으로 채우며 복원액 = min(남은 환수액, 버킷 사용액)
for 버킷 in 사용이력(최신순):
    복원액 = min(남은 환수액, 버킷 사용액)
    if 버킷 만료일 < 오늘:
        만료손실 += 복원액  // 만료 버킷: 소생 불가 → 손실로 분리
    else:
        원장 잔액 복원(버킷, 복원액) + 환불 이력 기록  // 유효 버킷만 복원
    남은 환수액 −= 복원액

4. 헤더-라인 상태머신 + 이력 단일화

5. 합포장 수거 — 2단계 승인 + DB 레벨 중복 차단

-- 수거 중복 키: DB 가 계산해 저장하는 생성 컬럼 (GENERATED … STORED)
중복키 = CASE 합포장이면 'C:'
             번들 배송이면 'B:' + 번들 키
             그 밖이면 'D:' + 원배송 ID END
UNIQUE (클레임, 중복키, 삭제여부)  -- 같은 배송을 두 번 수거할 수 없다

6. 환불계좌 암호화

7. 배치 자동화 — 택배 추적

배치 주기 역할
수거 택배 추적 10분 배송추적 서비스 폴링 → 수거 배송완료 감지 시 수거·라인·마이너스 PO 일괄 완료 전파
재배송/반송 추적 10분 교환 재배송·반송 배송완료 감지 → 라인 종결 + 전체 라인 완료 시 헤더 자동 종결

설계가 만든 결과 (Result)

검증 결과

대상 테스트 결과
금액 배분 불변식 금액 배분 테스트 (jqwik property) 10/10 · 불변식 4개 × 각 10,000 케이스
기청구 수량·금액 집계 기청구 집계 테스트 (@MybatisTest·H2) 12/12
발주라인 클레임 조각 (헤더축) 조각 집계 테스트 · 축 드리프트 가드 테스트 5/5 · 3/3
검수↔클레임 차감 (배송 단위 귀속) 검수 차감 집계 테스트 7/7
서비스주문 취소 가능 구간 취소 정책 테스트 6/6
분할검수 금액 보존 분할검수 테스트 2/2
결재 종결 연동 결재 종결 테스트 3/3
마이너스 PO 트랙 승계 마이너스 PO 생성 테스트 1/1
수거·재배송 엑셀 양식 엑셀 양식 테스트 9/9
포인트 FIFO 환수 포인트 환수 테스트 7/7 · 보존식 property 2,000 케이스

검증이 실제 결함을 잡았다

불변식을 세우자 설계가 아니라 구현의 구멍이 드러난 사례다. 예시 테스트만 짰다면 분할검수 × 추가클레임이라는 조합을 떠올리지 못해 놓쳤을 것이다.

기각한 대안이 실제로 틀렸음을 반증했다

위 「검토한 대안」 의 기각 사유가 추측이 아니라는 것까지 확인했다. 기각 대안 2(매번 원본 기준 재계산)을 동일한 property 에 넣자:

검출 내용
5번째 시도에서 반례 shrinking 27단계 → 최소 반례 1원·2개·[1+1] — round(0.5)=1 이 두 번 → 2원 배분(잔여 음수)
랜덤 원본 반례 262,891원 · 58개 · 10회 분할 → 합계 262,890원 (1원 유실)

포인트 환수 동시성 실측 (2026-08-30)

실험 구성 결과
① 같은 주문 경합 한 주문에 취소 클레임 동시 20건 성공 1 / 거절 19 · 복원 정확히 1회(50,000) · 클레임 1건만 생성
② 같은 원장 버킷 경합 서로 다른 두 주문의 사용이력이 동일 버킷을 참조, 동시 20건(각 10) 성공 2 / 거절 18 · 원장 잔액 0 → 100,000 (= 50,000 × 2, 정확히 2회)

이 검증이 말하지 않는 것