충전 수수료를 충전 트랜잭션 단위로 매칭해 회계 역추적 해결
목차
충전 수수료 차감, 어느 시점 요율을 따라야 하나
결제 플랫폼에서 파트너가 잔액을 충전할 때 충전 수수료를 떼는 구조인데, 환불·취소 흐름에서 차감 기준이 애매했음. 충전 시점 요율을 박아두는 방식이었는데, 파트너 등급이 중간에 바뀌면 과거 충전건과 현재 차감액이 어긋남. 회계팀에서 "이 충전건이 그 차감인지 매칭이 안 된다"는 컴플레인이 들어와서 손을 댔음.
무엇이 문제였나
- 충전 시 수수료를 잔액에서 즉시 차감하던 코드가 일부 남아 있었음. PENDING 후 확정 시 차감하는 표준 흐름과 어긋남
- 환불 시 "어느 충전건의 수수료인지" 역추적이 안 돼서 단순 합계로만 차감
- 파트너 등급이 바뀐 케이스에서 차감액이 실제 부과액과 미세하게 안 맞음
요약하면 수수료 부과 단위가 충전 트랜잭션이 아니라 잔액 단위였다는 점이 화근.
어떻게 바꿨나
파트너의 최근 충전건을 역순으로 훑으면서 매칭 키를 넘기도록 했음. 컨트롤러 두 곳에서 들어오는 흐름을 한 서비스로 모았고, 쿼리는 충전 이력에서 가장 최근 PENDING/CONFIRMED 건을 픽하도록 LIMIT 처리.
-- 의사 쿼리
select charge_seq, fee_rate, fee_amount
from charge_history
where partner_id = :partnerId
and status in ('PENDING','CONFIRMED')
order by created_at desc
limit 1;
| 변경 전 | 변경 후 |
|---|---|
| 잔액에서 단순 차감 | 매칭된 충전건의 fee_amount 차감 |
| 충전 시점 요율 모호 | 충전건 자체에 요율 스냅샷 |
| 역추적 불가 | charge_seq로 1:1 매칭 |
API 두 개의 진입점이 같은 서비스를 타게 되니, 분기 로직이 컨트롤러에서 사라진 게 가장 컸음. 컨트롤러는 입력 검증과 응답 매핑만, 매칭과 차감은 서비스 한 곳으로.
배운 점
- 수수료는 트랜잭션 단위로 묶어야 함. 잔액 차감으로만 처리하면 회계 추적이 안 됨
- 요율 스냅샷을 충전건 row에 박아두면 등급 변경 이슈가 사라짐. 파생 계산보다 저장이 답일 때가 있음
- 컨트롤러 두 개에 분산된 로직은 결국 서비스로 합치게 되어 있음. 처음부터 합쳐 짤 걸 그랬다는 후회
남은 숙제
- 환불 시 매칭된 충전건이 이미 CANCELLED인 경우 fallback 정책이 아직 임시
- 같은 시각에 들어온 다중 충전건의 동시성 처리 — 일단 row lock으로 막아뒀지만 부하 시점 재검토 필요
- 회계 대시보드에서 "충전건 ↔ 차감건" 매칭 뷰를 따로 뽑을지 검토 중
PENDING/CONFIRMED 흐름 정비하면서 곁다리로 잔재 코드들을 같이 정리하게 됐음. 결제 도메인은 한 번 꼬이면 추적 비용이 폭증하는 영역이라, 매칭 키를 데이터에 박아두는 습관을 들여야겠다고 다시 새김.
다음
댓글 0
첫 댓글 달아줘.