자동화 slecs

결제 수익 화면에 전체요약 탭과 실시간 폴링 갱신 추가

목차

admin/system-revenue 영역에 전체요약 탭을 새로 추가하고, 5초 간격 폴링으로 자동 갱신되는 기능을 붙였다. 변경된 파일은 뷰/스타일 4개, 내부 클래스 1개, SQL 매퍼 1개. 숫자로 보면 작지만 실제 작업 흐름은 쿼리 설계부터 시작해서 화면 렌더링까지 레이어를 전부 통과하는 작업이었다.

왜 이 기능이 필요했나

기존 결제 수익 화면은 개별 항목 단위로 데이터를 보여주는 구조였다. 운영 관점에서는 "오늘 전체 집계가 얼마냐"를 한눈에 봐야 하는데, 그러려면 화면 여러 곳의 숫자를 직접 더해야 했다. 탭 하나 없는 게 아니라 아예 그 집계 자체를 API가 내려주지 않았던 거라, 단순 UI 추가로 끝나지 않았다.

실시간 갱신 요건도 같이 올라왔다. 결제 데이터는 수시로 들어오니 화면을 새로고침하지 않으면 보는 사람이 항상 과거 스냅샷을 보게 된다. 이걸 해결하는 방법이 크게 두 가지인데 - 폴링이냐, 이벤트 기반이냐 - 이번 케이스에서는 5초 폴링으로 결정했다. 이유는 단순하다. 이 화면의 주요 사용자는 내부 운영팀이고, 동시 접속자가 많지 않으며, 인프라 레벨에서 WebSocket/SSE를 추가로 열기 위한 준비가 안 된 상태였다. 폴링이 세련된 방법은 아니지만, 현재 구조에서 가장 비용이 낮은 선택이었다.

구현 흐름

설계 순서는 SQL → 클래스 → 화면 순으로 내려갔다. 화면부터 그리고 데이터를 맞추려 하면 나중에 쿼리를 억지로 구겨 넣는 상황이 생기기 때문에, 집계 정의부터 잡는 걸 선호한다.

SQL 매퍼에 집계 쿼리를 추가할 때 핵심은 기존 쿼리와 집계 기준을 일치시키는 것이었다. 날짜 기준, 상태값 필터, NULL 처리 방식이 조금이라도 다르면 개별 합산과 전체 집계가 안 맞는 상황이 생긴다. 금융 도메인에서 그게 제일 골치 아프다. 숫자가 1원이라도 다르면 "이 화면 틀린 거 아니냐"는 질문이 바로 날아온다.

-- 예시: 전체 집계 쿼리 작성 시 기존 상태값 필터와 기준을 동일하게 맞춘 패턴
SELECT
    SUM(amount) AS total_amount,
    COUNT(*)    AS total_count,
    status
FROM payment_history
WHERE settled_at BETWEEN #{startDate} AND #{endDate}
  AND status IN ('COMPLETED', 'PARTIAL_REFUND')
  -- 기존 개별 조회 쿼리의 WHERE 절과 동일하게 유지
GROUP BY status

내부 클래스에 메서드를 추가할 때는 기존 로직과 충돌 여부를 먼저 확인했다. 같은 트랜잭션 범위 안에 들어가야 하는지, 별도 조회로 분리해도 괜찮은지 판단하는 게 이 단계의 주된 작업이다. 집계 쿼리는 읽기 전용이고 side effect가 없어서 별도 메서드로 분리해도 무방했다.

화면단 폴링은 interval 기반으로 구현했다. 탭이 활성화된 상태에서만 폴링이 돌아야 하고, 탭을 벗어나거나 화면을 닫으면 clearInterval이 호출돼야 한다. 이걸 빠뜨리면 백그라운드에서 계속 요청이 나가고, 사용자가 탭을 여러 번 껐다 켜면 중복 interval이 쌓인다.

// 탭 활성화 시 폴링 시작, 비활성화 시 정리하는 기본 패턴
let pollTimer = null;

function startPolling() {
    if (pollTimer) return; // 중복 방지
    pollTimer = setInterval(() => {
        fetchRevenueSummary();
    }, 5000);
}

function stopPolling() {
    if (pollTimer) {
        clearInterval(pollTimer);
        pollTimer = null;
    }
}

검증과 설계 시 놓치기 쉬운 것들

구현 후 검증은 두 가지 축으로 했다. 하나는 "이 화면의 숫자가 맞냐", 다른 하나는 "기존 화면의 숫자가 깨지지 않았냐". 전자만 확인하고 끝내는 경우가 많은데, 새 쿼리가 DB 부하를 높이거나 기존 트랜잭션 타이밍에 간섭하면 엉뚱한 화면에서 문제가 생길 수 있다.

예외 케이스도 사전에 항목화해두고 체크했다.

케이스 처리 방식
집계 데이터가 0건 빈 상태 UI 표시, 에러 아님
권한 없는 접근 기존 인증 필터와 동일하게 처리
NULL 집계 결과 COALESCE로 0 처리 후 반환
폴링 중 네트워크 오류 조용히 무시, 다음 interval에 재시도
쿼리 지연으로 interval 중첩 요청 중 플래그로 중복 호출 방지

폴링 중 네트워크 오류를 어떻게 처리하냐는 UX 판단이기도 하다. 운영 화면이고 5초 뒤에 자동으로 다시 시도하니까, 매번 에러 토스트를 띄우는 건 오히려 노이즈다. 연속으로 N번 실패하면 알림을 띄우는 방식이 더 적절하다고 봤는데, 이번엔 일단 조용히 무시하는 걸로 마무리했다. 운영 중에 진짜 문제가 생기면 그때 개선하는 게 맞다.


이 작업을 하면서 다시 확인한 건, 금융/결제 도메인에서 "대충 맞는 것 같다"는 기준은 없다는 거다. 숫자가 맞거나 틀리거나 둘 중 하나고, 중간은 없다. 그래서 구현 전에 현재 화면 수치를 메모해두고, 구현 후에 같은 조건으로 다시 확인하는 루틴을 항상 유지한다. 스크린샷까지는 아니더라도 주요 수치를 텍스트로 기록해두면 "원래 이 값이 맞는 건지" 헷갈리는 상황을 줄일 수 있다.

커밋도 논리 단위로 쪼갰다. SQL 추가, 클래스 메서드 추가, 화면 렌더링 연동, 폴링 로직을 각각 분리했다. 나중에 특정 변경이 문제가 됐을 때 어느 커밋에서 깨졌는지 git bisect나 로그만 봐도 바로 찾을 수 있게. 작은 습관인데 디버깅 비용을 꽤 많이 줄여준다.

사내 서비스라서 외부 서비스보다 기준을 낮춰도 된다는 생각은 안 한다. 오히려 내부 사용자는 숫자를 믿고 의사결정을 하는 사람들이라, 화면이 틀리면 그 판단까지 틀리게 된다. 그 비용이 더 크다.

댓글 0

첫 댓글 달아줘.