LabHub
배우기 러닝패스 코스

주문은 100건인데 정산은 98건이다 · 금액이 갈라지는 자리 · 이론

금액 배분 — 1원이 어디로 가는지 정하는 일

LabHub 에서 이어서 보기

한 줄 요약

할인·쿠폰·포인트·배송비는 주문에 붙고 환불은 품목으로 나가기 때문에, 주문 단위 금액을 품목마다 나눠 붙인 배분표가 없으면 부분 취소·부분 반품·판매자 정산이 전부 근사값이 된다.

왜 이게 필요했나

5,000원 쿠폰을 쓴 주문에서 품목 하나만 취소하면 얼마를 돌려줘야 할까요. 정가를 돌려주면 고객은 쿠폰만큼 이득을 보고 우리는 그만큼 손해를 봅니다. 반대로 쿠폰 전액을 빼고 돌려주면 남은 품목에 붙어 있던 혜택까지 회수하는 셈이라 고객이 항의합니다. 정답은 그 품목이 실제로 받은 돈, 즉 실판매가 를 돌려주는 것입니다. 그러려면 주문에 붙은 혜택을 품목마다 미리 나눠 두어야 합니다.

이 배분표는 환불에만 쓰이는 것이 아닙니다. 여러 판매자의 상품이 한 주문에 섞여 있으면 판매자별 정산도 이 표에서 나옵니다. 부가가치세 계산도 품목별 세율이 다를 때 이 표가 근거입니다. 그래서 배분은 "필요할 때 계산하면 되는 값" 이 아니라 주문 시점에 확정해서 저장해야 하는 값 입니다. 나중에 다시 계산하면 그 사이에 정책이 바뀌어 다른 숫자가 나옵니다.

어떻게 동작하나

나누는 방법은 대체로 품목 금액 비율입니다. 문제는 반드시 나머지가 남는다는 것입니다.

쿠폰 5,000원을 세 품목(89,000 / 7,900 / 18,500)에 나눈다  89,000 / 115,400 × 5,000 = 3,855.28...   7,900 / 115,400 × 5,000 =   342.28...  18,500 / 115,400 × 5,000 =   801.55...  내림만 하면 3,855 + 342 + 801 = 4,998  → 2원이 사라진다

사라진 2원은 어디에도 없으므로 배분표의 합이 실결제 금액과 달라지고, 그 차이는 나중에 정산 대사에서 원인을 알 수 없는 잔액으로 나타납니다. 그래서 배분 함수가 반드시 가져야 할 첫 번째 성질은 나눈 값의 합이 나누기 전과 같다 는 것입니다. 흔히 쓰는 방법이 최대잔여법입니다 — 일단 전부 내림한 뒤, 남은 금액을 나머지가 큰 쪽부터 1원씩 나눠 줍니다. 나머지가 같을 때 누가 먼저 받는지까지 규칙으로 못 박아야 같은 입력에서 언제나 같은 표가 나옵니다.

계산은 정수로만 해야 합니다. 이진 부동소수점은 0.1 이나 0.08 같은 값을 정확히 담지 못하고, 그 오차가 마지막 1원을 바꿉니다. 사정은 [파이썬 문서의 부동소수점 산술](https://docs.python.org/3/tutorial/floatingpoint.html) 에 잘 정리돼 있습니다. 실수를 꼭 써야 한다면 십진 연산을 제공하는 [decimal 모듈](https://docs.python.org/3/library/decimal.html) 같은 도구를 쓰고, 그렇지 않다면 금액은 원 단위 정수로, 비율은 1,000분율 정수로 두는 편이 훨씬 안전합니다. 금액 × 80 ÷ 1000 은 오차가 없고, 어디서 버림이 일어나는지도 눈에 보입니다.

배분의 기준을 무엇으로 삼을지도 정해야 합니다. 품목 금액 비율이 가장 흔하지만, 수량 비율로 나누는 회사도 있고 정가가 아니라 할인 후 금액을 기준으로 삼는 회사도 있습니다. 어느 쪽이 옳다기보다 한 번 정하면 바꾸지 않는 것이 중요합니다 — 기준이 바뀌면 옛 주문의 환불액이 달라지기 때문입니다. 그래서 배분 기준과 잔돈 규칙은 코드 주석이 아니라 상품 정책 문서에 있어야 합니다.

현장에서 만나는 모습

배분이 한 번 더 흔들리는 자리가 부분 취소입니다. 쿠폰에 "5만원 이상 구매 시" 같은 조건이 붙어 있으면, 품목을 빼는 순간 남은 주문이 그 조건을 못 채울 수 있습니다. 그러면 이미 준 혜택을 되가져와야 하고, 배송비 무료 조건도 같이 깨집니다. 그래서 "5만원짜리를 취소했는데 왜 3만원만 들어오나요" 라는 문의가 생깁니다. 이 계산을 해 두지 않으면 상담원이 답할 수 없고, 계산은 해 두었는데 화면에 근거를 보여 주지 않아도 마찬가지입니다. 회수액은 환불 내역에 별도 줄로 보여 주는 것이 분쟁을 가장 많이 줄입니다.

배분 규칙을 정할 때 자주 빠뜨리는 것이 "결정적인가" 입니다. 같은 주문을 두 번 계산했을 때 같은 표가 나와야 합니다. 잔돈을 받는 순서를 정해 두지 않으면 사전의 순회 순서나 정렬 방식이 바뀌는 것만으로 배분표가 달라지고, 그러면 어제 만든 환불 내역과 오늘 다시 계산한 환불 내역이 1원씩 어긋납니다. 이런 차이는 크지 않아서 한참 지나서야 발견되고, 발견됐을 때는 이미 수만 건이 쌓여 있습니다. 그래서 배분 함수를 만들 때는 합이 보존되는지와 함께 같은 입력에서 같은 결과가 나오는지를 시험으로 못 박아 두어야 합니다.

심한 경우에는 되가져올 금액이 돌려줄 금액보다 큽니다. 그때 고객에게서 돈을 더 받을 수는 없으므로 환불액은 0원에서 멈추는데, 이런 주문은 사실 "부분 취소로 처리하면 안 되는 주문" 입니다. 업무 규칙으로 전체 취소를 안내하거나 부분 취소 자체를 막는 편이 낫습니다. 이런 규칙은 코드 리뷰에서 나오지 않고, 실제 데이터로 한 번 계산해 봐야 눈에 보입니다.

다음 실습에서 할 것

합이 보존되는 배분 함수를 직접 쓰고, 채점기가 그 함수를 불러 재료에 없는 입력으로도 성질을 확인합니다. 주문 13건의 혜택을 품목마다 나눠 붙이고, 내림만 했다면 얼마가 사라졌을지 재어 본 다음, 부분 취소로 쿠폰 조건과 배송비 무료 조건이 깨질 때 환불액이 어떻게 달라지는지 계산합니다.