반품/교환(클레임)
- 테스트
- 실측
- 헤더-라인-수거 3계층 상태머신
- 부분 클레임 금액 배분(잔여 비례+마지막 흡수)
- 마이너스 PO 정산
- 포인트 FIFO 환수
목차 6
개요
B2B 전용몰의 취소·반품·교환 전체 프로세스를 담당하는 기능. 주문(SO)·발주(PO)와 맞물려 구매자 신청 → 공급사 처리(승인·수거·검수·환불/재배송) → 관리자 감독의 3주체 워크플로우를 단일 도메인으로 설계했다.
배송 전 취소, 배송 후 반품(환불), 교환(맞배송)을 하나의 클레임 모델로 통합하되, 물리 처리(수거·검수·재배송)는 라인·박스 단위로 독립 추적한다.
| 헤더-라인-수거 3계층 | 3개 유형 / 32개 상태 | 마이너스 PO 정산 | 포인트 FIFO 환수 |
|---|---|---|---|
| 신청 · 실무 처리 · 물류 단위를 분리 | 취소 5 · 반품 13 · 교환 14 | 원 PO 불변 + 음수 PO 생성 | 만료 가드 + FOR UPDATE |
왜 이 설계가 필요했는가 (Problem)
B2B 클레임은 단순 환불이 아니다 — 부분 출고·여러 공급사·여신/포인트 결제·정산 추적이 얽혀, 단일 테이블·단순 환불 로직으로는 정합성이 깨진다.
이 난제들을 헤더-라인-수거 3계층 + 마이너스 PO + 잔여 기준 금액 배분 + FIFO 포인트 환수의 조합으로 해결했다.
테이블 설계 — 헤더-라인-수거 3계층
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회) |