개발 slecs

수수료 관리 화면 개선과 데이터 정합성 확보

목차

admin 영역 작업은 외부 서비스만큼 공들여야 한다고 생각하는데, 현실은 늘 그렇지 않다. 사용 빈도가 낮고 담당자가 정해져 있다 보니 "지금 동작은 하잖아"로 버텨온 화면이 쌓인다. 수수료 총관리 페이지도 그중 하나였다. 이번에 대시보드 링크 교체가 계기가 됐고, 어차피 건드리는 김에 화면 구조도 같이 손봤다.

뷰와 스타일 파일 각 1개씩 변경이 전부처럼 보이지만, 실제로는 그 전 단계에서 내부 클래스에 메서드를 추가하고 SQL 매퍼에 쿼리를 새로 작성하는 과정이 먼저였다. 화면 변경이 빙산 위쪽이고 데이터 레이어 작업이 아래쪽인 구조. 단순 UI 개선이라도 기존 API가 필요한 값을 충분히 내려주지 않으면 프론트에서 계산 로직을 억지로 얹게 되고, 그렇게 되면 나중에 기준이 API 쪽이냐 프론트 계산이냐로 갈리기 시작한다.

쿼리부터 설계한 이유

화면에서 보여줘야 할 숫자가 무엇인지 먼저 정리하고, 그 숫자가 어느 테이블에서 어떻게 집계되는지 SQL부터 작성한 다음, 그 결과를 담을 클래스 메서드를 추가하고, 마지막으로 화면에 연결하는 순서였다. 역순 설계라고 할 수 있는데, 이렇게 하면 뷰 레이어에서 "값이 없으면 어떻게 하지"를 즉흥적으로 처리하는 상황이 줄어든다.

SQL 매퍼에 집계 쿼리를 추가할 때 같이 확인한 것들:

  • NULL 처리: COALESCEIFNULL로 집계 결과가 NULL인 경우 기본값 처리
  • 인덱스 활용 여부: 조건절 컬럼에 인덱스가 있는지, EXPLAIN으로 풀 스캔 없는지 확인
  • 기존 집계 쿼리와 기준 일치: 다른 화면에서 같은 데이터를 다른 방식으로 집계하고 있지 않은지

세 번째가 가장 까다롭다. 레거시 코드에서는 같은 개념을 다른 이름으로 부르거나, 범위를 조금씩 다르게 잡은 쿼리가 여러 곳에 흩어져 있는 경우가 흔하다. 새 쿼리를 쓰기 전에 기존 쿼리들을 먼저 파악하고, 기준을 통일하거나 기존 집계를 재사용하는 방향으로 가는 게 맞다.

이번 쿼리는 대략 이런 구조였다:

SELECT
  fee_type,
  COALESCE(SUM(fee_amount), 0) AS total_fee,
  COUNT(transaction_id)        AS txn_count
FROM fee_transactions
WHERE status != 'CANCELLED'
  AND created_at BETWEEN #{startDate} AND #{endDate}
GROUP BY fee_type

status != 'CANCELLED' 같은 조건이 다른 화면의 집계 로직과 일치하는지가 핵심이었다. 조건이 살짝 달라지면 두 화면 숫자가 어긋나고, 그게 버그인지 원래 그런 건지를 다시 파고드는 시간이 생긴다. 처음부터 맞춰두는 게 훨씬 싸다.

내부 클래스에 추가한 메서드는 이 쿼리 결과를 DTO로 매핑해서 서비스 레이어에 넘기는 역할이었다. 기존 클래스에 메서드를 붙이면서 신경 쓴 건 기존 메서드와의 충돌 여부. 같은 파라미터를 다른 방식으로 처리하는 메서드가 이미 있으면 혼란이 생기기 때문에, 이름과 역할 경계를 명확하게 구분했다.

데이터 정합성을 지키는 방식

금융/결제 관련 admin 화면은 숫자 하나가 틀려도 "버그냐, 원래 그런 거냐"부터 따져야 하는 상황이 생긴다. 그 시간이 아깝고, 신뢰 문제로 이어진다. 정합성은 기능 구현 후에 체크하는 게 아니라 설계 단계에서 미리 고려해야 한다는 걸 이 도메인 작업할 때마다 다시 느낀다.

실시간 갱신 여부도 이 과정에서 결정했다. 수수료 총관리 페이지는 실시간 갱신이 필요하지 않다고 판단해서 폴링이나 웹소켓 없이 요청 시 조회하는 방식으로 유지했다. 만약 실시간 갱신이 필요한 화면이었다면 고려할 트레이드오프는 이렇다:

방식 장점 단점
폴링 (setInterval) 구현 단순, 서버 side 변경 없음 불필요한 요청 발생, 갱신 주기만큼 딜레이
SSE 단방향 스트림, HTTP 기반 연결 유지 비용, 재연결 처리 필요
WebSocket 양방향, 즉시성 높음 구현 복잡, 인프라 고려 필요

admin 내부 도구라면 폴링으로 30초-1분 간격이 대부분 충분하다. 리소스 대비 효과를 따지면 WebSocket까지 갈 이유가 별로 없고, SSE도 연결 수명 관리가 귀찮아서 단순 폴링 대비 명확한 이득이 있을 때만 꺼내는 편이다.

예외 케이스는 방어적으로 처리했다. 빈 데이터는 빈 상태 UI를 보여주도록 했고, 권한 없는 접근은 기존 미들웨어 레이어에서 이미 차단하고 있어서 뷰 레이어에서 중복 체크는 하지 않았다. NULL 값이 숫자 필드에 내려오는 경우는 API 응답 처리 시점에 기본값으로 변환해서 뷰에서 undefined 참조가 생기지 않게 막았다. 이런 처리를 뷰 직전까지 미루면 조건문이 템플릿 여기저기에 퍼진다.

검증과 커밋 습관

구현 후 직접 화면에서 동작을 확인했다. 새 화면에서 나오는 숫자를 기존 관련 화면과 교차 확인해서 불일치가 없는지 체크. admin 화면은 테스트 커버리지가 얇은 경우가 많아서 이 수동 검증 단계를 빠뜨리면 놓치는 게 생긴다. 기존 데이터가 깨지지 않았는지도 같이 봤는데, "깨지지 않았다"는 걸 확인하려면 변경 전 수치를 기록해뒀어야 비교가 된다.

그래서 변경 전에 현재 화면 상태를 스크린샷이나 수치로 간단히 메모해두는 습관이 있다. 수정 후에 같은 케이스로 확인하면 "바뀐 건지 원래 그랬던 건지" 혼란이 없다. 기억은 믿을 게 못 된다, 특히 금융 숫자는.

커밋은 논리적으로 독립된 단위로 쪼갰다. 이번 작업이라면:

  1. SQL 매퍼 쿼리 추가
  2. 내부 클래스 메서드 추가 및 서비스 연결
  3. 뷰/스타일 변경, 대시보드 링크 교체

세 커밋으로 분리하는 게 맞다. 한 커밋에 다 묶으면 나중에 뷰 변경만 revert해야 할 때 SQL 변경까지 같이 돌아간다. git bisect로 문제 커밋을 찾을 때도 범위가 좁은 커밋이 압도적으로 유리하다.

커밋 메시지는 "무엇을 바꿨다"보다 "왜 바꿨는지"를 담으려고 한다. 대시보드 링크 교체 보다 수수료 관리 진입 경로 일원화 - 이전 링크가 deprecated된 내부 페이지를 가리키고 있어 수정 쪽이 6개월 후에 봤을 때 훨씬 유용하다. 작업 당시에는 당연하게 느껴지는 맥락이 금방 휘발된다는 걸 이미 여러 번 경험했다.

사내 서비스를 만들다 보면 기능 하나가 단순히 화면에 버튼 하나 추가하는 것으로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 처리, 예외 방어, 화면 렌더링, 권한 체크가 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 조건에서 이상한 화면이 나온다. "대충 맞는 것 같다"로 넘어가면 나중에 반드시 다시 돌아온다. 금융/결제 도메인에서는 특히.

댓글 0

첫 댓글 달아줘.