파트너 수수료 계층별 요율 역전 방지 검증 추가
목차
수수료 계층 구조를 다루는 코드는 겉보기엔 단순해 보여도 속으로 꽤 예민한 편이다. 비율 몇 개 곱하고 더하는 게 전부인 것 같지만, 계층 간 순서 한 가지가 틀어지면 마진이 통째로 뒤집힌다. 이번에 작업한 내용은 딱 그 지점-계층별 요율이 올바른 방향으로 설정돼 있는지 검증하는 로직-을 보강하는 거였다.
왜 요율 역전이 문제가 되는가
파트너 수수료 구조는 유통 계층을 따라 요율이 달라지는 차등 모델이다. 하위 계층이 더 높은 요율을 부담하고, 상위 계층은 그 차액을 마진으로 가져간다. 직관적으로 표현하면 이렇다.
| 계층 | 요율 | 마진 구조 |
|---|---|---|
| 최하위 | 1.0% | 최대 부담 |
| 중간 | 0.8% | 최하위-중간 차액(0.2%) 수익 |
| 최상위 | 0.6% | 중간-최상위 차액(0.2%) 수익 |
이 구조가 정상적으로 유지되려면 계층을 올라갈수록 요율이 낮아야 한다. 그런데 운영 중에 설정 실수나 마이그레이션 오류로 순서가 뒤집히면 어떻게 될까. 중간 계층 요율이 최하위보다 높게 들어가 버리면, 계산 로직은 에러 없이 돌아가지만 마진이 음수로 나온다. 코드상 예외는 없고 숫자만 이상해지는 유형이라서, 테스트를 따로 두지 않으면 배치가 한두 번 돌고 나서야 정산 담당자가 발견하게 된다.
수수료 계산 자체는 단순하다. 거래 금액에 요율을 곱하고 건당 고정 수수료를 더한다.
// 비율 수수료 + 건당 고정 수수료 합산
long feeAmount = (long)(txAmount * feeRate) + perTxFee;
long 캐스팅 위치가 중요한데, 곱셈 결과를 내리기 전에 반올림 처리를 어디서 하느냐에 따라 계층별로 소액 오차가 누적될 수 있다. 건수가 많은 정산 배치에서는 이게 합쳐지면 유의미한 차이가 된다. 이번엔 그 부분보다 요율 순서 검증에 집중했지만, 반올림 정책은 언젠가 한번 짚어야 할 포인트다.
계층 요율 순서 검증 추가
validation 로직 자체는 복잡하지 않다. 계층 목록을 순서대로 순회하면서 인접한 두 계층의 요율을 비교해 상위 계층 요율이 하위보다 크거나 같으면 예외를 던진다. 아이디어는 단순하지만 이걸 어디서 실행하느냐가 관건이다.
선택지를 정리하면:
- 수수료 설정 저장 시점에 즉시 검증 - 잘못된 설정이 DB에 들어가지 않는다. 운영 중 설정 변경을 막을 수 있어서 가장 선제적.
- 정산 배치 실행 직전에 검증 - 설정이 들어간 뒤라도 실제 계산 전에 막는다. 설정 저장과 배치 실행 사이에 시간 간격이 있어서, 두 번째 방어선 역할.
- 계산 함수 내부에서 매 호출마다 검증 - 확실하지만 성능 부담이 있다. 건수가 많을수록 누적된다.
이번엔 설정 저장 시점과 배치 실행 직전 두 군데 모두 걸었다. 중복처럼 보일 수 있는데, 레이어가 분리된 시스템에서는 한쪽이 빠진 경로로 요청이 들어올 수 있어서 두 곳 다 막는 게 맞다고 판단했다.
private void validateTierRateOrder(List<CommissionTier> tiers) {
for (int i = 0; i < tiers.size() - 1; i++) {
CommissionTier lower = tiers.get(i); // 더 낮은 계층
CommissionTier upper = tiers.get(i + 1); // 더 높은 계층
if (upper.getRate() >= lower.getRate()) {
throw new IllegalStateException(
String.format("계층 요율 역전: %s(%.4f) >= %s(%.4f)",
upper.getTierName(), upper.getRate(),
lower.getTierName(), lower.getRate())
);
}
}
}
예외 메시지에 어느 계층이 문제인지, 실제 요율 값이 얼마인지까지 담았다. 운영 중 alert 받았을 때 로그만 봐도 어디를 고쳐야 하는지 바로 알 수 있게 하려고.
정산 배치 구조와 이런 검증이 가지는 의미
정산 배치는 주기적으로 돌면서 해당 기간 거래 내역을 집계하고 수수료를 확정한다. 실시간이 아닌 배치라는 점이 중요한데, 한번 확정된 정산은 되돌리기가 번거롭다. 취소 처리, 파트너 고지, 경우에 따라선 이미 지급된 금액 환수까지 이어질 수 있다.
그래서 이런 류의 validation은 "실행하면서 버그를 잡는" 형태보다 "실행 전에 상태를 검증하는" 형태가 훨씬 낫다. 배치가 절반쯤 돌다가 예외로 죽는 것보다, 시작 전에 전제 조건을 체크하고 명확한 이유와 함께 멈추는 게 복구도 쉽고 원인 추적도 빠르다.
작업 규모 자체는 크지 않았다. 하지만 정산처럼 돈이 오가는 로직 주변에는 이런 사전 검증이 하나씩 붙어있어야 한다. 코드가 동작한다는 것과 올바르게 동작한다는 것 사이의 간격을 좁히는 작업이고, 그 간격이 좁을수록 배치 실패 후 야간 호출 받을 일이 줄어든다.
댓글 0
첫 댓글 달아줘.