개발 slecs

정산 잔액 화면에 페이지네이션과 어드민 디자인 개선

목차

admin/merchant-balance 영역은 정산 잔액 현황을 관리자가 조회·처리하는 화면인데, 건수가 쌓이면서 한 페이지에 전체를 뿌리는 구조가 한계에 부딪혔다. 로딩이 느려지는 건 그나마 참을 수 있는데, 데이터 건수가 많아질수록 DOM도 무거워지고 관리자가 원하는 row를 찾기 위해 브라우저 Ctrl+F를 쓰는 상황이 됐다. 그걸 보고 "이건 고쳐야 한다"고 판단했고 이번 작업으로 이어졌음.

변경된 파일 범위는 내부 클래스 1개, SQL 매퍼 1개, 뷰+스타일 각 1개. 겉으로 보면 작은 작업이지만 금융/결제 도메인에서는 쿼리 레벨부터 건드리는 순간 검증 범위가 넓어진다.

페이지네이션을 설계할 때 실제로 고민한 것들

페이지네이션은 구현 자체가 어렵지 않다. 문제는 어떤 방식을 선택하느냐다. 크게 두 가지 선택지가 있다.

방식 장점 단점
offset 기반 (LIMIT N OFFSET M) 구현 단순, 임의 페이지 이동 가능 데이터 삽입/삭제 시 row 밀림, 대용량에서 느림
cursor 기반 (마지막 PK 기준) 일관된 결과, 대용량에서도 인덱스 풀활용 임의 페이지 이동 불가, 구현 복잡도 증가

어드민 화면에서 "127페이지로 바로 가야 한다"는 요구는 거의 없다. 주로 최근 데이터를 순서대로 훑는 패턴이라 cursor 방식이 더 적합하다고 봤는데, 이번엔 기존 쿼리 구조와 정렬 조건이 복합 컬럼이라 cursor 전환 비용이 크다 판단해 offset으로 유지했다. 대신 조회 대상 범위를 조건으로 좁혀서 실제 OFFSET이 크게 커지지 않도록 쿼리를 작성했다.

SQL 매퍼에는 총 건수 쿼리(COUNT(*))와 실제 데이터 쿼리를 분리해서 작성했다. 하나의 쿼리로 전체를 가져온 뒤 앱단에서 자르는 방식은 의미가 없으니까. 내부 클래스 쪽에는 페이지 번호·사이즈·총 건수를 다루는 메서드를 추가하고, 뷰에서 페이지네이션 컴포넌트로 바인딩했다.

// 내부 클래스에 추가된 메서드 (실제 코드와 다를 수 있음 — 패턴 예시)
public PageResult<MerchantBalance> getBalancePage(int page, int size, BalanceSearchCond cond) {
    int offset = (page - 1) * size;
    int total = mapper.countBalance(cond);
    List<MerchantBalance> items = mapper.selectBalancePage(cond, offset, size);
    return new PageResult<>(items, total, page, size);
}

PageResult 같은 래퍼 클래스를 쓰면 뷰에서 "총 몇 건 중 몇 페이지"를 계산할 때 로직이 한 곳에 모여서 관리가 편하다. 뷰마다 나눗셈 계산을 흩뿌리면 나중에 로직이 어긋날 여지가 생긴다.

어드민 디자인 톤을 정리한 이유

페이지네이션이랑 같이 어드민 디자인 톤도 정리했다. 이쪽이 사실 더 오래 신경 쓴 부분이다.

사내 어드민은 외부 서비스 화면과 달리 브랜딩 제약이 적어서 빠르게 만들다 보면 화면마다 버튼 색상, 테이블 행 높이, 상태 배지 스타일이 조금씩 달라지는 경향이 있다. 처음엔 "어차피 내부 도구"라고 넘기는데, 결국 관리자들이 "이 화면은 뭔가 다른 느낌"이라고 하거나 신규 화면 만들 때 어느 스타일을 따라야 할지 기준이 없어진다.

merchant-balance 화면도 다른 어드민 화면들과 미묘하게 달랐다. 이번에 스타일을 맞추면서 내부적으로 어드민 공통 스타일 클래스를 좀 더 적극적으로 쓰는 방향으로 정리했다. 세부 수치는 테이블이 적합한데:

  • 테이블 헤더 배경색, 행 hover 색상, 상태 배지 색상 체계를 다른 어드민 화면과 통일
  • 버튼 계층(primary/secondary/danger)을 명시적으로 구분
  • 페이지네이션 컴포넌트 위치를 다른 화면과 동일하게 정렬

화면 하나에 적용한 것이지만 나중에 신규 화면 추가할 때 이 화면을 레퍼런스로 쓰면 된다는 게 이번 작업의 부수적인 성과다.

금융 도메인 특유의 검증 루틴

구현 후 화면에서 직접 동작 확인하는 건 기본이고, 이 도메인에서는 숫자 cross-check가 필수다. 정산 잔액이 다른 화면에서 보이는 값과 단 1원이라도 다르면 바로 신뢰 문제로 이어진다.

확인 순서는 대략 이렇게 했다:

  • 페이지네이션 적용 전 총 건수와 적용 후 전 페이지 합산 건수가 일치하는지
  • 1페이지 첫 row, 마지막 페이지 마지막 row가 기존 정렬 기준과 동일한지
  • 빈 결과(0건), 1건, 페이지 사이즈 정확히 일치하는 건수(예: size=20에 20건)일 때 컴포넌트가 이상하게 동작하지 않는지
  • NULL 처리: 잔액이 NULL로 들어오는 케이스에서 숫자 표시가 깨지지 않는지

마지막 케이스는 실제 데이터에서 종종 나오는데 "NULL이면 0으로 보여주면 되지"라고 단순하게 처리하면 나중에 "이 0은 진짜 0인가 데이터 없는 건가"가 애매해진다. 케이스별로 명시적으로 핸들링해두는 게 맞다.

쿼리 실행 계획도 변경 전후로 확인했다. 인덱스가 제대로 타는지 확인하는 것, 특히 OFFSET이 커질 때 풀 스캔으로 떨어지는지 여부를 미리 잡아두면 나중에 "갑자기 느려졌는데 왜?"를 방지할 수 있다.


기능 하나를 추가하는 작업이 SQL 집계, 상태 머신, 예외 처리, 화면 렌더링, 권한 체크를 전부 엮는다는 걸 이 도메인에서는 매번 다시 실감한다. 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나온다. "대충 맞는 것 같다"는 판단은 금융 화면에서는 없는 선택지다. 결국 다시 돌아오고, 돌아올 때는 더 복잡해진 상태로 돌아온다.

작업 단위도 이번에 논리적으로 나눠서 커밋했다. SQL 매퍼 추가, 내부 클래스 메서드 추가, 뷰 연동, 디자인 정리를 섞어서 한 번에 커밋하면 나중에 뭐가 문제였는지 찾기가 훨씬 힘들어진다. 커밋 메시지에는 "무엇을"보다 "왜"를 담으려 했는데, 이게 몇 달 뒤에 이 커밋을 다시 봤을 때 컨텍스트를 복원하는 데 실제로 도움이 된다.

댓글 0

첫 댓글 달아줘.