계층별 수수료 일괄 등록
목차
수수료 계산 로직은 건드릴 때마다 긴장된다. 금액이 틀리면 바로 민원으로 튀어나오고, 버그가 조용히 누적되면 나중에 추적하기도 어렵다. 이번 작업도 규모 자체는 크지 않았지만, 처음부터 제대로 잡아두지 않으면 꼭 나중에 다시 돌아오는 종류의 로직이었다.
작업 내용은 유통 계층별 수수료를 일괄 등록하는 기능이다. 계층 구조 자체는 이미 있었고, 각 계층에 요율과 건당 수수료를 묶어서 한 번에 등록하는 경로가 없었다. 기존엔 하나씩 수작업으로 넣어야 했는데, 계층이 늘어나거나 요율이 바뀔 때마다 실수가 생길 여지가 컸다.
계층 수수료 구조가 왜 복잡한가
원리 자체는 단순하다. 하위 계층이 높은 요율을 부담하고, 상위 계층은 그 차액을 마진으로 가져간다. 3계층짜리 구조를 예로 들면 이렇다.
| 계층 | 요율 | 비고 |
|---|---|---|
| 최하위 | 1.0% | 최대 부담 |
| 중간 | 0.8% | 차액 0.2% 수익 |
| 최상위 | 0.6% | 차액 0.2% 수익 |
여기에 건당 고정 수수료가 추가된다. 비율 수수료만 있으면 소액 거래에서 수익이 거의 안 남기 때문에, 현실적으로는 비율 수수료 + 건당 고정 수수료 조합이 표준에 가깝다.
// 수수료 계산 기본 패턴
long feeAmount = (long)(txAmount * feeRate) + perTxFee;
이 한 줄처럼 보이지만 실제로는 안에 여러 결정이 들어가 있다. 캐스팅 위치, perTxFee의 단위가 원인지 전인지, txAmount가 부가세 포함 금액인지 여부. 로직 자체보다 전제 조건을 확인하는 작업이 더 오래 걸릴 때가 많다.
특히 요율을 double로 관리하고 있으면 부동소수점 오차가 금액 계산에서 튀어나온다. 건당 오차가 1원이라도 거래가 수만 건이면 무시 못 하는 숫자가 된다. 나중에 BigDecimal 전환 작업이 생기면 또 일이 된다. 처음부터 아래처럼 정책을 명시적으로 박아두는 게 낫다.
BigDecimal fee = txAmount.multiply(feeRate)
.setScale(0, RoundingMode.FLOOR) // 수수료는 내림 - 부담자 유리
.add(perTxFee);
RoundingMode.FLOOR 한 줄이 사실상 정책 문서 역할을 한다. "왜 내림이에요?"라는 질문이 들어왔을 때 코드 보여주면 끝난다. 일반적으로 원 단위 절사는 수수료를 내는 쪽에 유리하게 처리한다. 올림으로 처리하면 부담 쪽에서 지속적으로 손해를 보고, 계층이 많아질수록 누적 차이가 분쟁 소지가 된다.
Validation이 실제로 막아주는 것
이번에 계층별 요율 순서 검증 로직도 같이 추가했다. 하위 계층 요율이 상위보다 낮으면 마진이 음수가 된다. 이게 DB에 그냥 저장되고 배치가 돌아버리면 정산 금액이 역전되는 상황이 생긴다. 실수 하나가 전체 계층 정산을 이상하게 만드는 구조라서, 배치 실행 전이 아니라 등록 단계에서 차단하는 게 맞다.
등록 시점에 체크해야 할 항목을 정리하면:
- 요율 범위 - 0 이상, 상한선 이하인지
- 계층 간 순서 - 하위 요율이 상위 요율 이상인지
- 건당 수수료 음수 여부
- 적용 기간 중복 - 같은 계층에 겹치는 구간이 있는지
마지막 항목이 은근히 자주 빠진다. 수수료 등록을 UI에서 여러 번 할 수 있는 구조라면 기간이 겹치는 케이스가 반드시 생기고, 이걸 안 막으면 배치가 어느 요율을 써야 할지 모호해진다. 배치에서 최신 등록 건을 우선시하는 fallback을 두는 경우도 있는데, 그렇게 하면 등록 실수를 배치가 조용히 덮어버린다. 그것보다는 등록 단계에서 막고 명시적으로 오류를 내는 편이 훨씬 명확하다.
정산 배치와 멱등성
정산 배치는 주기적으로 실행되면서 해당 기간 거래 내역을 집계하고 수수료를 확정한다. 여기서 멱등성이 중요하다. 같은 기간을 두 번 실행해도 결과가 동일해야 한다.
실무에서 이걸 깨는 패턴이 있다.
- 배치 실행 기록 없이 매번 새로 insert
- 중단됐다가 재시작할 때 이미 처리한 건을 다시 처리
- 수수료 확정 후에도 원본 거래 데이터가 수정될 수 있는 구조
제일 자주 보이는 건 첫 번째다. 정산 결과를 저장할 때 (기간, 대상) 복합키로 upsert하거나, 실행 전 기존 데이터를 delete 후 insert하는 방식 중 하나를 일관되게 가져가면 된다. 중요한 건 "이미 돌았으면 넘어간다"는 로직이 명확히 있어야 한다는 것. 배치가 새벽에 실패해서 재실행했더니 금액이 두 배가 됐다는 사태는 생각보다 흔하다.
세 번째 패턴도 주의가 필요하다. 정산이 확정된 이후에 원본 거래 상태가 바뀔 수 있으면 정산 기준 시점을 명확히 잡아야 한다. 확정 시점의 스냅샷을 따로 저장하거나, 확정 이후 원본 수정을 막는 상태 컬럼을 두는 방식으로 풀 수 있다.
정산 화면에서 챙길 것
정산 화면에서 사용자가 보는 건 결국 숫자다. 총 거래액, 수수료, 실수령액. 이 세 가지가 명확하게 보이는 것만으로도 CS 문의가 많이 줄어든다.
금액 표기에서 빠지기 쉬운 것들:
- 천 단위 구분자 일관 적용 및 '원' 단위 명시
- 마이너스 금액 빨간색 처리
- 수수료 내역을 비율 수수료 / 건당 수수료로 분리 표기
마지막 항목이 의외로 효과가 크다. "수수료가 왜 이만큼이에요?"라는 질문에 비율 수수료: X원 + 건당 수수료(N건): Y원 = Z원 형태로 보여주면 바로 이해한다. 합산값만 보여주면 납득하기까지 설명이 길어진다.
작업 자체는 크지 않았다. 계층 구조가 이미 있는 상태에서 일괄 등록 경로를 뚫고, 검증 로직 얹고, 배치 연동 확인한 정도. 그런데 이런 게 이후에 요율이 바뀌거나 계층이 추가될 때 훨씬 편하게 해준다. 운영하면서 손이 덜 가는 코드가 결국 좋은 코드라는 걸 이런 작업 때마다 다시 느낀다.
댓글 0
첫 댓글 달아줘.