출금 수수료 차등 정산 로직
목차
출금 수수료 차등 정산 로직을 작업했다. 규모는 크지 않았지만, 이 종류의 변경은 건드리기 전에 구조를 완전히 이해하고 들어가야 한다. 금전 관련 계산 버그는 발견 시점이 항상 늦고, 늦으면 복구 비용이 기하급수적으로 올라간다.
차등 수수료 구조가 생기는 이유
유통 계층이 생기면 수수료도 계층화된다. 상위 계층일수록 더 많은 물량을 처리하거나 더 높은 신용을 인정받아 낮은 요율을 적용받고, 그 차이가 상위 계층의 마진이 되는 구조다. 직관적으로는 단순한데, 코드로 내려오면 꽤 많은 예외가 붙는다.
우리 시스템의 수수료 구조는 아래처럼 생겼다.
| 계층 | 요율 | 비고 |
|---|---|---|
| 최하위 | 1.0% | 최대 부담 |
| 중간 | 0.8% | 차액 0.2% 수익 |
| 최상위 | 0.6% | 차액 0.2% 수익 |
여기서 중요한 건 계층 간 요율의 순서 보장이다. 하위 계층 요율이 상위보다 낮아지는 순간, 상위 계층이 마진을 내는 역전 현상이 생긴다. 운영 중에 요율 설정을 잘못 건드리면 바로 이 상황이 된다. 그래서 validation을 추가했다 - 단순한 체크지만, 없으면 설정 실수가 데이터로 조용히 쌓인다.
수수료는 비율 수수료와 건당 고정 수수료를 합산한다. 코드로 보면:
// 비율 수수료 + 건당 고정 수수료 합산
long feeAmount = (long)(txAmount * feeRate) + perTxFee;
// 계층 요율 순서 validation
if (lowerTierRate < upperTierRate) {
throw new IllegalStateException(
"하위 계층 요율이 상위보다 낮습니다: lower=" + lowerTierRate + ", upper=" + upperTierRate
);
}
(long) 캐스트 위치가 중요하다. txAmount * feeRate 계산을 double로 먼저 하고 그 결과를 long으로 내리는 게 의도다. 순서가 바뀌거나 중간에 다른 캐스트가 끼어들면 소수점 처리 결과가 달라질 수 있다.
올림/버림이 만드는 분쟁
원 단위 반올림 정책은 코드 한 줄이지만 실제로는 정책 결정이다. 관행적으로는 수수료를 내는 쪽에 유리한 방향, 즉 내림을 적용한다. 수수료를 조금이라도 덜 내는 게 사용자 입장에서는 맞고, 그 차이가 수 원 수준이라도 정책이 없으면 나중에 "왜 이 금액이냐"는 문의가 생긴다.
문제는 계층이 여러 개일 때다. 각 계층의 마진 계산이 상위 계층 요율과 하위 계층 요율의 차에 거래 금액을 곱한 값인데, 각 단계에서 내림을 적용하면 전체 합산 시 소수점 오차가 계층 수만큼 쌓일 수 있다. 이걸 어느 시점에 버릴지를 명확히 정하지 않으면 계층별 정산 합계와 전체 수수료 합계가 1-2원씩 안 맞는 케이스가 생긴다. 금액 자체는 작아도, 정산 검증 쿼리에서 항상 차이가 나오면 운영 부담이 된다.
우리는 각 계층의 수수료를 독립적으로 계산하고 버리는 방식을 택했다. 정산 합계와 거래 합계가 완전히 일치하지 않을 수 있지만, 그 차이가 어디서 왔는지 명시적으로 문서화해뒀다.
정산 배치의 멱등성
배치 작업에서 멱등성은 선택이 아니다. 네트워크 오류나 타임아웃으로 같은 배치가 두 번 실행되는 상황은 언제든 생길 수 있고, 그때 결과가 달라지면 수수료가 이중으로 차감되거나 정산 레코드가 중복 생성된다.
멱등성을 보장하는 방법 중 가장 단순한 건 정산 대상 기간 + 계층을 복합 유니크 키로 걸고, 이미 정산된 건이 있으면 업데이트 없이 그냥 넘기는 방식이다. 재실행 자체를 허용하되 결과를 덮어쓰지 않는 것.
INSERT INTO settlement (period, tier, fee_amount, tx_count, created_at)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE fee_amount = fee_amount -- no-op
좀 더 정교하게 가려면 정산 상태를 DRAFT, CONFIRMED 같은 단계로 나누고, CONFIRMED 이후에는 재처리를 막는 방어 코드를 넣는다. 이번 작업에서는 기본 멱등성 보장 수준으로 마무리했다.
정산 UI에서 보여줄 것들
정산 화면에서 사용자가 보는 건 결국 세 숫자다. 총 거래액, 수수료, 실수령액. 이 셋이 명확하게 보이고, 셋을 합산하면 맞아야 한다는 신뢰를 줘야 한다.
천 단위 구분자와 '원' 단위는 기본이고, 마이너스 금액은 색상으로 즉시 구분되게 처리하는 게 좋다. 빨간색이 직관적이다. 정산 기간과 거래 건수도 함께 노출하면 "왜 이 금액이냐"는 문의를 줄일 수 있다.
작은 변경이었지만 계층 구조, 올림/버림 정책, 멱등성 이 세 가지를 제대로 정리하는 계기가 됐다. 이런 류의 로직은 처음 설계할 때 가정을 명시적으로 문서화해두지 않으면 나중에 맥락 없이 숫자만 남는다. 코드 옆에 왜 이 방향으로 계산했는지 주석으로 남겨두는 게 이 경우엔 특히 유효했다.
댓글 0
첫 댓글 달아줘.