계층별 수수료율 차등 계산
목차
수수료 계산 로직은 건드릴 때마다 손이 좀 떨린다. 숫자가 틀리면 바로 돈 문제로 직결되고, 그게 신뢰 문제로 이어지기 때문이다. 3월 27일 작업도 규모 자체는 크지 않았지만, 그래서 오히려 더 꼼꼼하게 들여다봤다.
계층 차등 요율 구조가 생기는 이유
유통망이 여러 계층으로 나뉘어 있을 때 수수료는 단순히 "얼마 떼는가" 이상의 의미가 생긴다. 하위 계층이 상위 계층보다 높은 요율을 부담하고, 그 차액이 상위 계층의 수익이 되는 구조. 당연해 보이지만, 코드로 구현할 때 이 관계가 깨지는 순간 마진이 음수가 되는 버그가 조용히 발생한다.
이번에 적용한 계층 구조는 아래 형태였다.
| 계층 | 요율 | 마진 |
|---|---|---|
| 최하위 | 1.0% | - |
| 중간 | 0.8% | 0.2% (최하위와의 차액) |
| 최상위 | 0.6% | 0.2% (중간과의 차액) |
이 테이블만 보면 단순한데, 실제로 "요율 순서 검증"을 코드에 명시적으로 넣지 않으면 운영 중에 누군가 관리 화면에서 요율을 잘못 입력했을 때 상위 계층이 오히려 더 떼는 역전 현상이 생긴다. 그게 즉시 터지면 그나마 다행이고, 배치 돌고 나서야 발견되면 이미 여러 건이 잘못 정산된 후다.
그래서 validation을 추가했다. 계층 요율을 저장하거나 변경할 때, 상위 계층 요율이 반드시 하위 계층보다 낮은지 체크하도록. 사소해 보이지만 이런 방어 로직이 없으면 언젠가 반드시 당한다.
수수료 계산의 디테일 - 비율과 건당의 합산
수수료가 비율만 있는 게 아니라 건당 고정 수수료도 함께 붙는 구조라 계산이 살짝 복잡해진다. 핵심 공식은 이렇다.
// 비율 수수료 + 건당 고정 수수료 합산
long feeAmount = (long)(txAmount * feeRate) + perTxFee;
여기서 (long) 캐스팅이 사실상 내림(floor) 처리다. 소수점 이하는 그냥 잘린다. 이게 의도적인 선택인지, 아니면 그냥 타입 변환인지 명확히 하는 게 중요한데, 수수료를 부담하는 쪽에 유리한 방향으로 내림 처리하는 게 일반적인 관행이다. 수수료를 덜 내는 방향으로 계산한다는 게 직관적으로도 합리적이고, 나중에 분쟁이 생겼을 때 "우리가 적게 받았다"고 설명하는 게 훨씬 낫다.
반올림 정책을 코드에서 implicit하게 처리하면 나중에 로직을 읽는 사람이 "이게 의도된 건지 실수인지" 헷갈린다. 아래처럼 명시적으로 표현하는 편이 낫다.
// 명시적 내림 처리 - 수수료 납부자에게 유리한 방향
long rateBasedFee = (long) Math.floor(txAmount * feeRate);
long feeAmount = rateBasedFee + perTxFee;
차이는 미미하지만 코드 읽는 사람한테 의도가 전달된다는 게 다르다.
정산 배치와 멱등성
정산 배치는 주기적으로 돌면서 해당 기간 거래 내역을 집계하고 수수료를 확정한다. 여기서 반드시 잡아야 하는 속성이 멱등성이다. 같은 기간을 두 번 실행해도 결과가 달라지면 안 된다.
실무에서 이게 깨지는 패턴은 대체로 두 가지다.
- 중복 집계: 배치 실행 상태를 제대로 관리하지 않아서 이미 처리된 거래가 다음 배치에 다시 집계됨
- 부동소수점 누적 오차: 합산 순서에 따라 부동소수점 오차가 달리 쌓여서 미묘하게 결과가 달라짐
전자는 실행 로그와 상태 컬럼으로 막을 수 있다. 배치 실행 시작 시 해당 기간 처리 여부를 먼저 확인하고, 이미 확정된 기간이면 건너뛰거나 명시적 재실행 플래그가 있을 때만 덮어쓰는 방식이다. 후자는 long으로 전환하거나 BigDecimal을 쓰는 방향으로 해결한다. 금액 계산에서 double은 원칙적으로 피해야 한다.
정산 화면 얘기를 빼놓기 어려운데, 사용자가 결국 보는 건 숫자다. 총 거래액, 수수료, 실수령액 세 가지를 한눈에 파악할 수 있어야 하고, 금액 표기에서 천 단위 구분자와 '원' 단위를 일관되게 적용하는 건 기본이다. 마이너스 금액은 빨간색 처리해서 별도로 읽지 않아도 직관적으로 구분되게 하는 것도 마찬가지. 이런 건 개발자한테는 사소해 보이지만, 정산 담당자가 매일 보는 화면이라 누적 피로도에서 차이가 크다.
작업 규모 자체는 크지 않았다. 근데 이런 류의 변경이 실제로 더 까다롭다. 명확한 기능 추가가 아니라, 기존 로직에 검증 레이어를 얹고 엣지케이스를 막는 일이라서 "잘 됐는지" 확인하는 기준이 모호하다. 틀리지 않았음을 증명하는 테스트를 같이 짜는 게 이런 작업에서 유일한 안전망이다.
댓글 0
첫 댓글 달아줘.