개발 slecs

이용중계수수료 계층별 요율 정산 로직

목차

이용중계수수료는 유통 계층이 여러 겹인 B2B 서비스에서 자주 등장하는 구조다. 내가 작업한 케이스는 계층별로 요율이 다르게 적용되고, 각 계층은 자기 아래 계층과의 요율 차이로 마진을 가져가는 방식이었음. 개념 자체는 단순해 보여도 테이블 설계, 정산 배치, 관리 화면까지 붙다 보면 신경 써야 할 포인트가 생각보다 많다. 3월 말에 이 작업을 마무리하면서 남겨두고 싶었던 것들을 정리함.

계층 요율 구조와 validation

수수료 구조는 위쪽 계층일수록 요율이 낮다.

계층 요율 비고
최하위 1.0% 최대 부담
중간 0.8% 차액 0.2% 수익
최상위 0.6% 차액 0.2% 수익

이 구조가 성립하려면 반드시 하위 계층 요율 > 상위 계층 요율이어야 한다. 어긴 순간 중간 계층의 마진이 음수가 된다. 그래서 저장 시점에 validation을 넣었음. 요율 순서가 뒤집혀 있으면 저장 자체를 막는 방식.

// 계층 요율 순서 검증 - 인덱스 0이 최상위 계층 기준
for (int i = 0; i < tierList.size() - 1; i++) {
    if (tierList.get(i).getFeeRate() >= tierList.get(i + 1).getFeeRate()) {
        throw new IllegalArgumentException(
            "상위 계층 요율은 하위 계층보다 낮아야 합니다. index=" + i
        );
    }
}

이걸 빠뜨리면 데이터 입력 실수 하나가 조용히 음수 마진 데이터를 만들어낸다. 정산 배치가 돌기 전까지는 아무도 모름. 발견하는 건 이미 정산이 완료된 뒤고, 그때부터 소급 추적이 복잡해진다. 금전 로직에서 입력 단계 검증이 중요한 이유가 바로 이것.

건당 수수료 합산과 반올림

이번 계산식은 비율 수수료와 건당 고정 수수료 두 가지를 합산하는 구조다.

// 비율 수수료 + 건당 고정 수수료 합산
long feeAmount = (long)(txAmount * feeRate) + perTxFee;

여기서 (long) 캐스팅이 소수점을 버리는 내림 처리를 한다. 금융 계산에서 반올림 방향은 관행적으로 수수료 납부자에게 유리하게, 즉 내림으로 가져간다. 수수료를 걷는 쪽이 조금 손해 보는 것처럼 보이지만, 분쟁 소지를 없애는 게 장기적으로 낫다.

다만 (long) 캐스팅에는 함정이 있다. 부동소수점 연산 특성상 1000 * 0.019.999...가 되는 경우가 있어서, 이론상 10원이 나와야 하는 계산이 9원으로 떨어질 수 있음. 규모가 작을 때는 오차가 묻히지만 건수가 쌓이면 무시 못한다. 명시적인 반올림 모드가 필요한 상황이라면 BigDecimal을 쓰는 게 더 안전하다.

// BigDecimal로 반올림 모드 명시
BigDecimal fee = new BigDecimal(txAmount)
    .multiply(feeRateDecimal)
    .setScale(0, RoundingMode.FLOOR);
long feeAmount = fee.longValue() + perTxFee;

계층이 여러 겹이면 각 계층마다 반올림 오차가 누적된다. 어느 계층에서 최종 금액을 확정할지를 설계 시점에 정해두는 편이 낫다. 보통은 가장 아래 계층 계산에서 한 번만 내림 처리하고, 상위 계층 마진은 그 결과에서 단순 차감으로 확정하는 방식이 깔끔함.

정산 배치의 멱등성

정산 배치는 주기적으로 실행하면서 해당 기간 거래를 집계하고 수수료를 확정한다. 여기서 멱등성이 핵심이다. 같은 기간을 두 번 돌려도 결과가 동일해야 함.

멱등성을 깨는 패턴은 보통 두 가지다.

  • 정산 상태 체크 없이 무조건 INSERT - 중복 정산 레코드가 쌓인다
  • 집계 쿼리가 이미 정산된 거래를 필터링하지 않음 - 다음 배치 실행 시 동일 건이 또 잡힌다

방어는 간단한 편이다. 배치 실행 전에 해당 기간-계층 조합으로 이미 확정된 레코드가 있으면 스킵하거나, UPSERT로 처리하면 된다. 배치 로그에 실행 시각, 대상 기간, 처리 건수를 남겨두면 재실행 이력 추적도 수월해짐.

JSP 관리 화면은 계층별 요율을 직접 입력하고 수정하는 용도로 만들었다. 저장 요청 시 서버 사이드에서 위에 말한 validation이 다시 돌고, 이미 정산이 확정된 기간의 요율은 수정 못하도록 막았음. 소급 적용을 허용하면 이미 끝난 정산 결과가 달라지는 거라 잠가두는 게 맞다.

작업 규모 자체는 크지 않았다. 테이블 몇 개, 배치 로직, 관리 화면. 그런데 금전 계산이 엮이는 순간 검증과 예외처리에 들어가는 공이 기능 구현보다 커진다는 건 할 때마다 다시 느낀다. validation 하나 빠졌으면 음수 마진 데이터가 조용히 쌓였을 거고, 발견은 정산 완료 한참 뒤였을 거다. 이런 방어 코드가 티 안 나는 이유가, 잘 되면 아무 일도 없어 보이기 때문이다.

댓글 0

첫 댓글 달아줘.