개발 slecs

결제 대시보드에 정산 원장 뷰·실시간 KPI·CSV 내보내기 추가

목차

system-revenue 영역에 세 가지 기능을 한 번에 붙였다. 정산 원장 스타일로 대시보드를 리빌드하고, 실시간 KPI 카드를 얹고, CSV 내보내기까지 추가했음. 건드린 파일은 내부 클래스 하나, SQL 매퍼 하나, 뷰/스타일 하나. 숫자만 보면 작은 작업인데, 실제로는 기존 화면과 API에서 제공하지 않던 데이터와 동작이 필요해져서 쿼리 레벨부터 다시 설계해야 했다. 단순 UI 추가가 아니라 데이터 모델 선택부터 시작하는 작업이었음.

왜 원장 스타일로 리빌드했나

기존 대시보드는 당기 합계만 보여주는 구조였다. 관리자 입장에서 "이 금액이 왜 이렇게 찍혔는지"를 추적하려면 별도 쿼리를 직접 돌려야 했음. 원장(ledger) 뷰로 전환한 건 이 추적 흐름을 화면 안으로 가져오기 위해서다. 입금/출금/잔액을 행별로 펼쳐놓으면 특정 시점 잔액의 근거가 명확해지고, 이상값이 생겼을 때 어느 트랜잭션에서 어긋났는지 빠르게 특정할 수 있다.

결제 도메인에서 합산 숫자만 보여주는 화면은 절반짜리다. 숫자가 맞아도 왜 맞는지를 설명할 수 없으면 신뢰하기 어렵고, 틀렸을 때 원인을 찾는 데 시간이 과하게 걸린다. 원장 구조는 이 검증 비용을 줄여준다는 게 핵심이었음.

SQL 매퍼에서는 집계 쿼리를 두 레이어로 나눴다. 행별 원장 데이터를 뽑는 쿼리와 그 위에서 KPI를 집계하는 쿼리를 분리해서 관리함.

-- 원장 행 조회 (패턴 예시)
SELECT
    t.id,
    t.settled_at,
    t.type,          -- 'CREDIT' | 'DEBIT'
    t.amount,
    SUM(t.amount) OVER (
        PARTITION BY t.account_id
        ORDER BY t.settled_at
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS running_balance
FROM settlements t
WHERE t.account_id = #{accountId}
  AND t.settled_at BETWEEN #{from} AND #{to}
ORDER BY t.settled_at, t.id;

윈도우 함수로 누적 잔액을 계산하면 잔액 필드를 별도로 관리하지 않아도 된다. 잔액 컬럼을 직접 UPDATE하는 방식은 동시 트랜잭션 타이밍에 따라 순간적으로 틀린 값이 저장될 수 있는데, SUM OVER는 조회 시점에 계산하므로 그 리스크가 없음. 정합성 측면에서 훨씬 안전한 패턴이다. 단, 데이터가 많아지면 성능 부담이 생기기 때문에 구현 전에 실행 계획을 확인했고, settled_ataccount_id에 인덱스가 제대로 걸려 있는지 대조했음. 범위가 커질 경우 페이지 단위 조회로 나눠서 로드하는 방식도 검토 대상으로 남겨뒀다.

실시간 KPI: 폴링 방식을 고른 이유

KPI 카드를 "실시간"으로 만들 때 가장 먼저 결정해야 했던 건 갱신 방식이었다. 폴링과 이벤트 방식을 두고 잠깐 고민했는데, 지금 단계에서는 폴링으로 갔다.

방식 장점 단점 결정
폴링 구현 단순, 서버 상태 무관 불필요한 요청 발생 채택
SSE / WebSocket 변경 즉시 반영, 요청 최소화 연결 관리, 인프라 추가 부담 보류
캐시 + 무효화 DB 부하 최소화 캐시 일관성 관리 복잡 향후 검토

내부 관리 도구에서 초 단위 정밀도가 필요한 상황이 아니었고, 수십 초 단위 갱신으로도 충분했음. SSE나 WebSocket은 연결 유지, 재연결 로직, 서버사이드 브로드캐스트 구조가 추가되는데, 그 복잡도를 지금 단계에서 끌어들이는 건 과한 투자였다. 폴링 주기는 나중에 조정할 수 있게 설정값으로 분리해뒀음.

KPI 집계 쿼리는 원장 쿼리와 분리해서 내부 클래스에 별도 메서드로 뒀다. 하나의 메서드가 원장 데이터 조회와 KPI 집계를 동시에 처리하면 재사용이 어려워지고 단위 테스트도 불편해짐. 역할을 나눠두면 KPI 계산 로직을 격리해서 검증하기도 훨씬 편하다.

CSV 내보내기와 마무리 검증

CSV는 단순해 보이는데 결제 데이터에서 함정이 몇 개 있다.

  • 금액 표현: 부동소수점으로 직렬화하면 1234.501234.51234.4999...로 나오는 케이스가 생긴다. 화면에서는 포맷팅으로 가리지만 CSV에선 그대로 드러남. BigDecimal 계열로 유지하거나 문자열로 직렬화하는 게 안전하다.
  • 인코딩: 한글 컬럼명이 들어가면 엑셀에서 열었을 때 깨지는 경우가 있음. BOM(\uFEFF)을 앞에 붙이거나, UTF-8임을 명시하지 않으면 Windows 기본값으로 열려서 깨지는 케이스를 감안해야 한다.
  • 컬럼 순서: 쿼리 결과 순서 그대로 내보내면 실제 업무에서 쓰기 불편한 경우가 많다. 현업에서 쓰는 정산서 양식과 컬럼 순서를 맞췄음.
  • NULL 처리: 빈 값을 빈 셀로 남길지, "0"으로 채울지, "-"로 표기할지는 도메인마다 다르다. 결제 데이터에서 금액 NULL과 0은 의미가 다르기 때문에 구분해서 처리했다.

구현 후 직접 화면에서 동작을 확인했다. 기존 데이터가 깨지지 않았는지, 관련 화면에서 보이는 숫자와 원장 합산이 일치하는지 cross-check했음. 변경 전 수치를 메모해두고, 수정 후 같은 케이스로 확인하는 것, 관련 화면 숫자를 대조하는 것이 습관처럼 굳어진 이유가 있다. 결제 도메인에서 "대충 맞는 것 같다"로 넘어가면 나중에 반드시 다시 돌아온다. 숫자 하나가 틀리면 신뢰가 무너질 수 있어서 꼼꼼함이 기본값이어야 한다는 걸 작업할 때마다 다시 확인하게 됨.

커밋도 논리적으로 독립된 단위로 쪼개서 냈다. 내부 클래스 변경, SQL 매퍼 추가, 뷰/스타일 연동을 별도 커밋으로 나눠두면, 나중에 어느 변경에서 무언가 깨졌는지 찾을 때 범위를 빠르게 좁힐 수 있다. 커밋 메시지도 "무엇을"보다 "왜"를 담으려고 신경 씀. 코드 자체는 diff에서 읽힌다. 메시지까지 "추가했음"으로 끝내면 맥락이 사라진다.

사내 서비스를 만들다 보면 기능 하나가 화면에 버튼 하나 추가하는 것으로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 처리, 예외 방어, 화면 렌더링, 권한 체크가 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나타난다. 이번 작업도 파일 세 개짜리로 보이지만 그 안에 설계 결정이 꽤 많이 쌓여 있다.

댓글 0

첫 댓글 달아줘.