가맹점 정산 누적 수익 조회에 PG 누적 비용 항목 추가
목차
merchant-withdraw 영역에서 platform-revenue/cumulative 응답에 PG 누적 비용 컬럼을 추가했다. 변경 파일은 SQL 매퍼 1개. 규모는 작지만 금융 도메인이라 고려할 게 생각보다 많았다.
누적 수익 조회 화면에서 가맹점이 "실제로 손에 쥐는 금액"을 파악하려면 수익 합계만으로는 부족하다. PG사가 가져가는 수수료가 빠진 숫자는 그럴듯해 보이지만 실제 정산 기준과 괴리가 생긴다. 이 항목이 없으면 담당자가 별도로 뽑아서 계산해야 하는 상황이 된다는 걸 확인했고, 화면에서 바로 줘야 하는 데이터라는 게 명확했다.
컬럼 하나 추가처럼 보이지만, PG 비용은 결제 건당 발생하고 집계 기간·상태 필터에 따라 포함 범위가 달라진다. 취소/환불 건에 붙은 PG 수수료를 어떻게 처리할지, 부분 취소의 경우 비용 배분을 어떻게 볼 건지 같은 엣지 케이스가 먼저 정리되어야 쿼리를 짤 수 있었다. 이 결정이 선행되지 않으면 나중에 숫자 불일치 이슈로 반드시 돌아온다.
SQL 집계 - NULL 방어와 상태 필터 정합성
누적 집계 쿼리에서 자주 나오는 실수가 NULL을 그냥 SUM에 밀어넣는 것이다. PG 비용 컬럼이 모든 레코드에 채워져 있다는 보장은 없다. 결제 수단에 따라 PG를 거치지 않는 케이스가 섞여 있을 수도 있고, 구형 데이터일수록 컬럼이 비어 있을 가능성이 높다.
SELECT
merchant_id,
SUM(revenue_amount) AS cumulative_revenue,
COALESCE(SUM(pg_fee_amount), 0) AS cumulative_pg_cost,
SUM(revenue_amount)
- COALESCE(SUM(pg_fee_amount), 0) AS net_revenue
FROM settlement_summary
WHERE merchant_id = #{merchantId}
AND settled_at BETWEEN #{fromDate} AND #{toDate}
AND status IN ('SETTLED', 'PARTIAL_SETTLED')
GROUP BY merchant_id
COALESCE(..., 0) 하나가 없으면 PG 비용 컬럼이 NULL인 레코드가 하나라도 섞이는 순간 SUM 전체가 NULL로 떨어진다. 금융 쿼리에서는 방어 코드가 과하다 싶을 정도로 명시적으로 쓰는 게 맞다고 생각한다. 이번에도 COALESCE를 붙이고, 상태 필터 조건을 기존 누적 수익 쿼리와 완전히 동일하게 맞추는 데 신경을 많이 썼다. 집계 조건이 조금이라도 어긋나면 수익 합계에서 비용을 뺀 값이 다른 화면의 실 정산액과 달라지고, 그게 생기는 순간 "이 화면 숫자 왜 달라요?" 질문이 날아온다.
집계 날짜 기준도 체크 포인트다. settled_at 기준으로 집계해야 할 걸 created_at 기준으로 짜면 기간 경계에서 숫자가 어긋난다. 기존 매퍼의 조건을 그대로 참조해서 맞췄기 때문에 이번엔 크로스체크에서 바로 통과됐지만, 새로 쿼리를 짤 때는 항상 의심하고 확인해야 한다.
실시간 vs 배치 - 이 숫자는 언제 봐야 하나
이런 누적 집계를 추가할 때마다 나오는 질문이 있다. "이 숫자, 실시간으로 봐야 하나?" 폴링이나 이벤트 드리븐으로 매번 새로 계산하면 항상 최신 값이지만 DB 부하가 따라온다. 배치로 미리 집계해두면 쿼리는 가볍지만 갱신 주기만큼 지연이 생긴다.
| 기준 | 실시간 집계 | 배치/확정 집계 |
|---|---|---|
| 데이터 신선도 | 즉시 반영 | 주기적 지연 |
| DB 부하 | 조회 시마다 발생 | 집계 시점에 집중 |
| 적합한 화면 | 진행 중 거래 모니터링 | 확정 정산 내역 조회 |
PG 누적 비용은 누적 정산 조회 화면에 붙는 항목이고, 이미 확정된 과거 데이터를 보는 맥락이다. 실시간으로 바뀔 이유가 없다. 이 판단이 서면 쿼리 부하를 줄이는 방향으로 설계할 수 있고, 기존 누적 조회 흐름과 동일한 방식으로 집계하는 쪽을 선택했다.
실시간이 필요한 경우는 "지금 이 순간의 상태"가 핵심일 때다. 진행 중인 거래 잔액이나 현재 가용 한도 같은 것들. 누적 수익·비용처럼 기간을 정해서 뒤돌아보는 조회라면 굳이 실시간 부하를 감수할 이유가 없다. 이 트레이드오프를 미리 정리해두지 않으면 나중에 성능 이슈가 생겼을 때 왜 이 구조를 선택했는지 설명하기 어려워진다.
검증 루틴과 커밋 전략
구현 후 직접 화면에서 동작을 확인했다. 이때 신규 컬럼만 보는 게 아니라 기존 수익 합계 숫자도 같이 봤다. 같은 조건에서 PG 비용 컬럼을 추가했을 때 기존 숫자가 달라지면 안 된다. 그 다음엔 (수익 합계 - PG 누적 비용)이 다른 화면에서 보이는 실 정산 합계와 일치하는지 확인했다. 숫자가 세 화면에서 모두 일치하면 그때서야 "됐다"고 판단할 수 있다.
작업 단위를 커밋으로 쪼개는 것도 같은 맥락이다. 이번엔 SQL 매퍼 수정, 응답 객체 필드 추가, 렌더링 연동을 각각 다른 커밋으로 분리했다. 나중에 숫자 이슈가 생겼을 때 어느 레이어에서 깨졌는지 git log만 봐도 범위를 좁힐 수 있으면 디버깅 시간이 절반 이상 줄어든다. 변경 전 동작을 메모해두고, 수정 후 동일한 케이스로 다시 확인하는 루틴도 유지하고 있다. 귀찮아 보이지만 금융 화면에서 이 단계를 생략하면 반드시 나중에 갚아야 한다.
커밋 메시지도 "PG 비용 컬럼 추가"보다는 "확정 정산 내역에서 실수령액 파악을 위한 누적 PG 비용 집계 추가" 쪽으로 이유를 담으려 했다. 코드는 diff를 보면 뭘 했는지 알 수 있지만, 왜 했는지는 메시지 없이는 3개월 뒤 본인도 모른다.
사내 서비스를 만들다 보면 기능 하나가 단순히 화면에 버튼 하나 추가하는 것으로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 필터, 예외 처리, 화면 렌더링, 권한 체크가 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나온다. 특히 금융/결제 도메인은 숫자 하나가 틀리면 시스템 신뢰가 흔들린다. 작은 변경일수록 검증 루틴을 생략하지 않으려고 의식적으로 노력하는 이유가 거기 있다. "대충 맞는 것 같다"는 판단은 결제 화면에서 쓸 수 없는 기준이다.
댓글 0
첫 댓글 달아줘.