미정산 파트너 자동 정지
목차
정산이 밀린 파트너를 수동으로 확인하고 처리하다 보면 반드시 구멍이 생긴다. 담당자가 바뀌거나, 공휴일이 끼거나, 다른 일이 겹치면 누락이 발생한다. 그게 한 건이면 그냥 처리하면 되는데, 쌓이기 시작하면 나중에 훨씬 복잡한 상황이 된다. 미정산 기간이 길어질수록 파트너 쪽에서도 정지 이유를 납득하기 어렵고, 대응 비용이 올라간다. 그래서 배치로 자동화하기로 했다.
이번 작업의 핵심은 두 가지였다. 미정산 상태를 판별하는 기준을 코드로 명확히 정의하는 것, 그 기준에 해당하는 파트너를 주기적으로 정지 처리하는 배치를 추가하는 것. 규모가 큰 작업은 아니었지만, 운영 안정성 측면에서 방치하면 안 되는 영역이었다. 수수료 계산 로직도 이 과정에서 같이 정리했다.
수수료 계층 구조와 계산
수수료 구조는 유통 계층별로 요율이 다르게 설정되는 차등 모델이다. 하위 계층이 더 높은 요율을 부담하고, 그 차액이 상위 계층의 수익이 되는 방식이다.
| 계층 | 요율 | 비고 |
|---|---|---|
| 최하위 | 1.0% | 최대 부담 |
| 중간 | 0.8% | 차액 0.2% 수익 |
| 최상위 | 0.6% | 차액 0.2% 수익 |
구조 자체는 단순해 보이지만, 계층 수가 늘어날수록 계산 오류가 생기기 쉽다. 특히 계층 순서가 뒤집히는 경우, 즉 하위 계층 요율이 상위보다 낮아지면 마진이 음수가 된다. 버그가 터지기 전까지는 조용히 손실이 쌓이는 유형이라 사전 validation을 넣었다.
for (int i = 0; i < tiers.size() - 1; i++) {
if (tiers.get(i).getFeeRate() <= tiers.get(i + 1).getFeeRate()) {
throw new IllegalStateException(
"계층 요율 오류: 하위 계층 요율이 상위 이하여야 함 (index=" + i + ")"
);
}
}
설정을 잘못 넣어도 배치가 시작부터 예외를 던지고 멈춘다. 조용히 틀린 계산을 계속하는 것보다 시끄럽게 실패하는 쪽이 낫다. 금전 로직에서 Fail fast 원칙은 오류 발견을 빠르게 하고, 롤백 범위도 최소화해준다.
수수료 계산 자체는 비율 수수료에 건당 고정 수수료를 더하는 방식이다.
// 비율 수수료 + 건당 고정 수수료 합산
long feeAmount = (long)(txAmount * feeRate) + perTxFee;
이 한 줄에서 신경 써야 할 게 생각보다 많다. (long) 캐스팅은 소수점 이하를 버리는 내림 처리다. 수수료를 내는 쪽 기준으로는 유리한 방향이고, 서비스 입장에서는 수백만 건이 쌓이면 미세한 차이가 누적된다. 올림/버림 정책을 명확히 정하고 코드 주석으로 남겨두는 게 나중에 분쟁 대응할 때 근거가 된다. 일반적으로 수수료 계산은 내는 쪽에 유리한 방향(내림)을 원칙으로 삼는 게 이후 분쟁을 줄인다. 받는 쪽도 올림 없이 정확하게 받으면 양쪽 모두 납득하기 쉽다.
정산 배치의 멱등성
정산 배치에서 가장 중요한 속성은 멱등성이다. 같은 기간을 두 번 실행해도 결과가 달라지면 안 된다. 서버 재시작, 스케줄러 중복 실행, 수동 재실행 등 실제 운영에서 배치가 두 번 돌아가는 상황은 생각보다 자주 발생한다. 이걸 코드 레벨에서 보장하지 않으면, 언젠가 이중 정산 문제가 터진다.
멱등성 보장 방식은 크게 두 가지다.
- 처리 상태 컬럼 + 스킵: 정산 테이블에
status컬럼을 두고, 이미COMPLETED상태면 건너뛴다. 단순하고 명확하다. - Upsert 처리: 같은 키로 재실행하면 동일한 값으로 덮어써지도록 설계한다. 결과가 동일하면 덮어쓰는 게 무해하고, 중간 실패 후 재실행에도 안전하다.
이번엔 전자로 구현했다. 정산 기간별로 배치 실행 결과를 별도 테이블에 기록하고, 배치 시작 시 해당 기간이 이미 처리됐는지 먼저 확인한다. 배치가 중간에 실패하면 실행 기록이 남지 않으니 재실행이 가능하다.
자동 정지 처리도 같은 원칙을 적용했다. 이미 정지 상태인 파트너를 다시 정지 처리해도 에러 없이 넘어가도록 했다. 상태 전이 자체를 멱등하게 만들면 배치 오류를 추적할 때 훨씬 편하다. 실패 원인을 찾다가 "재실행했더니 상태가 이상해졌다"는 상황을 피할 수 있다.
정산 배치는 주기적으로 실행되면서 해당 기간 거래 내역을 집계하고 수수료를 확정하는 흐름이다. 집계 범위를 어떻게 잡느냐도 중요한데, 경계 조건을 잘못 설정하면 첫날 또는 마지막 날 거래가 누락되거나 중복 집계된다. 날짜 범위는 항상 시작 시각 포함, 종료 시각 미포함([start, end))으로 일관되게 처리하는 게 가장 실수가 적다.
정산 화면에서 숫자 표기
배치 작업과 별개로, 정산 UI 쪽도 같이 정리했다. 파트너가 정산 화면에서 가장 많이 보는 건 결국 세 가지 숫자다: 총 거래액, 수수료, 실수령액.
이 숫자들을 명확하게 표기하는 것만으로도 문의가 줄어든다. 천 단위 구분자, 단위(원) 표기, 마이너스 금액 빨간색 표시를 일관되게 적용하면 직관적으로 읽힌다. 실수령액이 예상보다 적어서 문의가 오는 경우, 대부분은 수수료 항목이 분리 표시되지 않아서 생기는 혼선이다. 총액과 수수료, 실수령액 세 줄을 명확히 분리하면 이런 문의 자체가 줄어든다.
자동 정지 처리된 파트너에게는 정지 사유와 미정산 금액을 명시적으로 노출해야 한다. "왜 정지됐는지 모르겠다"는 문의는 자동화로 해결할 수 있는 영역이 아니라, 알림과 화면 표시로 선제적으로 막아야 한다. 정지 처리 시점에 파트너에게 알림을 보내고, 정산 화면에 미정산 금액과 정지 해제 조건을 명시하면 운영팀으로 들어오는 문의 자체가 달라진다.
이번 작업에서 배운 건 결국 금전 로직은 방어적으로 짜야 한다는 거다. Fail fast, 멱등성, validation 먼저, UI에서 숫자 명확하게. 이 네 가지가 지켜지면 운영 중에 터지는 잡음이 눈에 띄게 줄어든다. 오류 하나가 신뢰 손상으로 직결되는 영역이라, 방어 코드와 검증에 투자하는 시간은 나중에 다 회수된다.
댓글 0
첫 댓글 달아줘.