개발 slecs

계층별 수수료 정산 로직에 검증 추가로 오정산 방지

목차

계층별 수수료 정산은 구조 자체가 단순해 보여도 구현 실수가 생기면 조용히 오래 가는 유형의 버그다. 이번 작업은 기능 추가라기보다 기존 로직에 안전망을 덧댄 것인데, 작업하면서 생각보다 점검할 게 많았다.

계층 구조가 왜 까다로운가

수수료 모델은 유통 계층별로 요율이 다르고, 하위 계층이 상위보다 높은 요율을 부담한다. 그 차액이 상위 계층의 수익이 된다. 표로 보면 명확하다.

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

이 구조는 설정이 DB나 설정 파일에 저장되어 있어서, 운영 중 누군가가 요율을 수정했을 때 순서가 역전될 수 있다. 예를 들어 중간 계층 요율을 0.5%로 내리면 최하위(1.0%)와의 마진은 괜찮은데, 최상위(0.6%)보다 낮아져서 중간이 최상위에 마진을 오히려 넘겨줘야 하는 상황이 생긴다. 계산 로직 자체는 아무 이상 없이 돌아가면서 음수 마진을 조용히 정산해버린다. 이게 배치가 돌고 나서 발견되면 수동 보정이 필요하고, 이미 정산이 완료된 이후면 더 복잡해진다.

그래서 이번에 추가한 건 정산 배치가 실행되기 전, 또는 요율이 업데이트될 때 계층 간 순서를 강제 검증하는 로직이다. 어느 계층도 자기 상위 계층보다 요율이 낮으면 안 된다는 조건을 명시적으로 체크하고, 조건을 만족하지 않으면 정산을 중단한다. 조용히 넘어가는 것보다 시끄럽게 실패하는 게 훨씬 낫다.

수수료 계산과 반올림 정책

수수료 계산 자체는 비율 수수료와 건당 고정 수수료의 합산이다.

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

(long) 캐스팅은 소수점을 버린다. Java에서 double을 long으로 강제 캐스팅하면 내림(floor)이 아니라 0 방향 절사(truncation)인데, 양수 거래금액에서는 사실상 내림과 동일하다. 수수료를 내는 쪽 입장에서 내림 처리는 유리한 방향이고, 원 단위 이하는 그냥 버리는 게 관행이기도 하다.

문제는 이 코드가 내림인지 절사인지 의도를 명시하지 않는다는 거다. 음수 거래(환불이나 차감)가 생기면 동작이 달라질 수 있다. 거래 도메인에 환불이 있다면 Math.floor()BigDecimal.ROUND_DOWN으로 의도를 명시하는 게 낫다.

// 의도를 명시하는 방식
long feeAmount = (long) Math.floor(txAmount * feeRate) + perTxFee;
// 또는 BigDecimal로 정밀도 제어
BigDecimal fee = txAmount.multiply(feeRate)
    .setScale(0, RoundingMode.FLOOR)
    .add(perTxFee);

금액 계산에서 double을 쓰는 게 찜찜하다면 BigDecimal이 맞는데, 성능 오버헤드가 있어서 처리량이 많은 배치라면 long으로 원 단위만 다루는 방식으로 설계하는 게 일반적이다. 어떤 방식을 선택하든 팀 내에서 통일하고 주석으로 남겨두는 게 나중에 건드리는 사람이 편하다.

배치 멱등성과 검증 순서

정산 배치는 멱등성이 필수다. 같은 기간을 두 번 돌렸을 때 결과가 달라지면 안 된다. 배치 실패로 재실행하는 경우가 반드시 생기고, 그때마다 중복 정산이 생기면 수습이 번거롭다.

일반적인 패턴은 이렇다.

  • 정산 대상 기간과 상태를 키로 갖는 정산 레코드를 먼저 생성하고, 이미 완료 상태면 스킵한다
  • 집계 쿼리는 트랜잭션 테이블 기준으로 기간 필터만 걸어서 동일한 결과가 나오도록 설계한다
  • 정산 중간 상태(처리중)를 별도로 두고, 완료 전에 재실행이 들어오면 처리중 상태를 확인해서 막는다

이번 작업에서 검증 추가가 이 흐름 안에서 어디 들어가는지도 중요했다. 계층 요율 검증은 정산 레코드 생성 직전에 두는 게 적절하다. 집계가 다 끝난 다음에 검증이 걸려서 롤백하면 이미 쓴 집계 비용이 낭비다. 검증 - 집계 - 확정 순서로 가는 게 맞다.

작업 규모 자체는 크지 않았다. 검증 로직 추가, 단위 테스트 케이스 몇 개, 그게 다다. 근데 이런 류의 작업이 미루다 보면 언젠가 실제로 터지는 버그의 씨앗이 된다. 금전 관련 로직은 "설마 누가 저렇게 설정하겠어"가 통하지 않는다. 방어 코드 하나가 나중에 밤에 전화 받는 상황을 막아준다.

댓글 0

첫 댓글 달아줘.