대시보드에 PG 실비용 일별·월별·누적 집계 추가
목차
결제대행사 비용을 실시간으로 파악할 수 없으면 운영이 블랙박스가 된다. PG사가 월말에 정산서를 보내주기 전까지 "이번 달 얼마 나갔는지" 모르는 상태로 서비스를 굴리는 건 꽤 불편한 일인데, 그게 지금까지의 현실이었다. 이번에 dashboard-pg 영역에 pgExpenseToday / pgExpenseMonth / pgExpenseTotal 세 컬럼을 추가하면서 이 문제를 해결했다.
변경 파일은 SQL 매퍼 하나뿐이다. 근데 그 한 파일 때문에 설계 시간이 꽤 걸렸다.
SQL 집계 설계: 간단해 보이는 쿼리가 의외로 안 간단한 이유
일별·월별·누적을 한 쿼리에서 뽑는 건 얼핏 SUM + GROUP BY 문제처럼 보이지만, 결제 도메인에선 몇 가지 함정이 있다.
첫째, "비용"의 기준이 뭔지를 먼저 확정해야 한다. 승인 시점 기준인지, 정산 확정 시점 기준인지, 취소·환불 건을 차감하는지. 이번엔 결제 승인 시점 기준에 취소 건 차감 방식을 택했다. 이 정의가 흔들리면 숫자가 화면마다 달라지고 나중에 어느 쪽이 맞는지 아무도 모르게 된다.
둘째, NULL 처리. 거래가 아예 없는 날은 SUM이 NULL을 반환한다. COALESCE(SUM(...), 0)을 빠뜨리면 프론트에서 null 값이 내려와 렌더링이 깨진다. 금융 도메인에서 화면에 - 나 undefined가 찍히는 건 숫자 오류보다 더 인상이 나쁘다.
셋째, 성능. 집계 쿼리는 테이블이 커질수록 느려진다. 작성 전에 실행 계획을 확인했고, 날짜 컬럼과 상태 컬럼에 복합 인덱스가 있는지 체크했다. 없었으면 추가해야 할 뻔했는데 다행히 기존 인덱스로 커버됐다.
구체적으로는 이런 형태의 구조로 짰다:
SELECT
COALESCE(SUM(CASE WHEN DATE(created_at) = CURDATE() THEN pg_fee ELSE 0 END), 0) AS pgExpenseToday,
COALESCE(SUM(CASE WHEN DATE_FORMAT(created_at, '%Y-%m') = DATE_FORMAT(NOW(), '%Y-%m') THEN pg_fee ELSE 0 END), 0) AS pgExpenseMonth,
COALESCE(SUM(pg_fee), 0) AS pgExpenseTotal
FROM payment_log
WHERE status IN ('APPROVED', 'PARTIAL_CANCEL')
AND is_deleted = 0;
CASE WHEN 으로 세 집계를 한 번의 풀스캔에서 뽑는 방식이다. 쿼리를 세 번 날리는 것보다 I/O가 훨씬 적다.
폴링 주기와 숫자 포맷 통일
실시간 갱신은 5초 폴링으로 처리했다. 웹소켓을 쓰거나 SSE를 도입하는 것도 고려했는데, 이 대시보드는 내부 운영 화면이라 동시 접속이 많지 않고, 1초 미만의 latency가 필요한 케이스도 아니어서 폴링으로 충분하다고 판단했다. 복잡도 대비 효과가 낮은 기술을 굳이 끌어들일 이유가 없다.
숫자 포맷은 다른 화면의 기준과 맞췄다.
| 항목 | 적용 기준 |
|---|---|
| 천단위 구분 | 콤마(,) |
| 통화 단위 | 원(₩) suffix |
| 소수점 | 절사 (원 단위 이하 표기 안 함) |
| 음수 (환불 초과) | 별도 스타일 없음, 그냥 음수 표기 |
이게 별거 아닌 것 같아도 화면마다 포맷이 다르면 "왜 저 화면은 1,234원이고 이 화면은 1234.0원이냐"는 질문이 반드시 나온다. 숫자 포맷 기준을 한 번 정해두고 공통 함수 하나로 통일해 두면 그 이후엔 신경 쓸 일이 없다.
검증과 정합성 확인
구현 후 제일 먼저 한 게 기존 화면과의 숫자 대조였다. 이미 다른 영역에서 비슷한 집계를 보여주는 화면이 있었고, 거기서 보이는 월별 합계와 이번에 새로 추가한 pgExpenseMonth가 같은 값을 가리키는지 확인했다. 다행히 일치했다.
일치하지 않았다면 어느 쪽이 맞는지를 판단해야 하고, 기준을 통일하는 작업이 추가로 필요했을 것이다. 금융 도메인에서 같은 지표가 화면마다 다른 숫자를 보여주는 건 신뢰 문제로 이어진다. 운영팀 입장에서 "어느 숫자를 믿어야 하냐"는 상황이 오면 그 시스템은 쓰기 힘들어진다.
검증 체크리스트를 뽑자면:
- 거래가 한 건도 없는 날:
pgExpenseToday가null이 아닌0으로 오는지 - 취소 건만 있는 날: 음수가 정상 출력되는지
- 월 경계 (말일 23:59 vs 1일 00:00):
pgExpenseMonth가 리셋되는지 - 페이지 진입 후 5초 경과: 폴링이 정상 갱신되는지
이 중 월 경계 케이스는 타임존 설정이 어긋나면 조용히 틀린 값이 나오는 케이스라서 특히 신경 썼다. DB 서버와 애플리케이션 서버 타임존이 다른 환경이면 NOW()와 CURDATE()가 기대와 다른 값을 반환할 수 있다.
기능 하나가 단순히 화면에 숫자 세 개 추가하는 것처럼 보여도, 그 뒤엔 쿼리 설계-포맷 통일-예외 처리-정합성 검증이 다 붙어 있다. 그걸 건너뛰면 나중에 반드시 다시 돌아온다. 금융 도메인에서 "대충 맞는 것 같다"는 통과 기준이 되면 안 된다는 걸 작업할 때마다 다시 확인하게 된다.
커밋은 SQL 쿼리 추가 / 포맷 함수 적용 / 폴링 연결 세 단계로 쪼갰다. 나중에 숫자가 이상해지면 어느 커밋에서 깨졌는지 bisect하기 훨씬 쉬워지기 때문이다. 작은 커밋을 자주 하는 습관은 코드 리뷰보다 사고 대응에서 더 빛을 발한다.
댓글 0
첫 댓글 달아줘.