개발 slecs

파트너 포털 대시보드에 회원수·KPI 현황 추가

목차

파트너 포털 대시보드에 전체 회원수와 KPI 현황을 추가했다. 변경 파일은 세 개 - 내부 클래스 하나, SQL 매퍼 하나, JSP+스타일 하나. 파일 수만 보면 작아 보이지만 손댄 레이어 수는 그보다 많다.

왜 지금 이걸 해야 했나

파트너 포털은 파트너들이 KPI를 모니터링하는 내부 대시보드다. 기존에도 지표들은 있었는데, 전체 회원수가 어디에도 노출되지 않았다. 운영 측에서 수치를 보려면 별도 관리자 화면을 열거나 DB를 직접 조회해야 했다.

단순한 불편함 문제가 아니다. "내 캠페인이 얼마나 많은 회원을 대상으로 하는가"라는 기준값이 없으면 나머지 KPI 수치가 맥락 없이 떠다니는 숫자가 된다. 전환율이 3%라고 할 때, 모수가 1만 명인지 100만 명인지에 따라 의미가 완전히 달라진다. 그래서 대시보드 기준점으로 전체 회원수를 넣고, 기존 차트들의 가독성을 같이 다듬었다.

구현: 쿼리부터 화면까지

SQL 매퍼부터 시작했다. 집계 쿼리를 새로 작성할 때 제일 먼저 하는 일은 실행 계획 확인이다. 대시보드 화면은 요청마다 여러 쿼리가 동시에 나가는 구조라서, 집계 쿼리 하나가 느리면 화면 전체가 버벅인다.

<!-- MemberMapper.xml -->
<select id="selectTotalMemberCount" resultType="int">
    SELECT COUNT(*)
    FROM member
    WHERE use_yn = 'Y'
      AND status_cd NOT IN ('WITHDRAWN', 'SUSPENDED')
</select>

이 정도 쿼리는 단순해 보이지만, 조건절 컬럼에 따라 인덱스 사용 여부가 갈린다. EXPLAIN으로 풀스캔이 아닌지 확인하고 반영했다.

서비스·컨트롤러 레이어에서는 기존 대시보드용 DTO에 회원수 필드를 추가하고, 서비스에서 집계 메서드를 호출해 채워 넣는 방식으로 처리했다. 별도 API를 새로 만들지 않고 기존 대시보드 조회 엔드포인트에 얹었다.

// DashboardService.java (요약)
public DashboardVO getDashboardData(String partnerId) {
    DashboardVO vo = dashboardMapper.selectKpiData(partnerId);
    vo.setTotalMemberCount(memberMapper.selectTotalMemberCount());
    return vo;
}

새 엔드포인트를 안 만든 이유가 있다. 대시보드 화면은 한 번 요청에 여러 지표를 한꺼번에 내려주는 구조다. 회원수만 따로 빼면 화면 쪽 AJAX 콜이 하나 더 생기고, 권한 인터셉터도 한 번 더 통과해야 한다. 불필요한 레이어 추가였다. 기존 파트너 인터셉터가 이미 이 엔드포인트를 커버하고 있어서 권한 처리는 그대로 통과 확인만 했다.

프론트(JSP + AJAX) 쪽은 기존 대시보드 JSP에 영역을 추가하는 방식이었다. 페이지 로딩 시 단일 AJAX로 대시보드 데이터를 받아 각 영역에 뿌리는 구조이므로, 회원수는 응답 객체에 이미 들어있어서 렌더링만 추가했다. 사이드바 메뉴 활성화 처리는 기존 active 클래스 바인딩 로직을 그대로 활용했다.

// 기존 success callback 안에 추가
$('#totalMemberCount').text(
    Number(data.totalMemberCount).toLocaleString() + '명'
);

숫자 포맷팅은 toLocaleString()으로 처리했다. 세 자리 콤마를 손으로 구현하면 꼭 엣지케이스가 나온다.

설계에서 고민한 지점들

항목 선택 이유
갱신 방식 화면 진입 시 1회 조회, 폴링 없음 실시간성 불필요, 불필요한 DB 부하 방지
API 분리 여부 기존 엔드포인트에 통합 요청 수 최소화, 권한 중복 확인 불필요
NULL 처리 COUNT 쿼리라 NULL 없음, JS에서 0 fallback 추가 방어 처리는 렌더링 레이어에서도 한 번 더
권한 체크 기존 파트너 인터셉터 통과 새 독립 메뉴가 아니라 기존 대시보드 확장

폴링이냐 이벤트냐는 매번 결론이 같지 않다. 이번 케이스는 "몇 분 전 데이터여도 무방한" 지표였고, 파트너 포털 특성상 동시 접속자가 많지 않아서 단순 조회로 충분했다. 반대로 실시간 잔액이나 재고 수량 같은 값이라면 얘기가 달라진다. 그때그때 "이 값이 5분 늦게 보여도 괜찮은가?"를 기준으로 판단하는 편이다.

예외 케이스 방어도 빠뜨리기 쉬운 부분이다. 새 기능을 추가할 때 정상 케이스만 테스트하고 넘어가면, 특정 파트너 계정이나 데이터가 비어있는 초기 상태에서 화면이 깨지는 걸 운영 중에 발견하게 된다. 빈 데이터, NULL, 권한 없는 접근은 구현 단계에서 방어 코드를 넣어두는 게 낫다.

검증 방식

구현 후 검증 순서는 항상 비슷하다.

  • 화면에서 직접 노출 수치 확인
  • 같은 데이터를 보는 다른 화면과 숫자 cross-check
  • NULL·빈 데이터 케이스 수동 테스트
  • 권한 없는 계정으로 접근 시도해서 차단되는지 확인

이번에도 관리자 화면에서 보이는 회원수와 포털 대시보드 숫자를 직접 비교했다. 당연히 같아야 하는데, 이걸 확인하지 않으면 나중에 두 화면이 다른 숫자를 보여줄 때 원인 추적부터 다시 해야 한다. 미리 잡는 게 압도적으로 싸다.

금융/결제 도메인에서 숫자 하나가 틀리면 신뢰가 무너질 수 있다. "대충 맞는 것 같다"로 넘어가면 반드시 다시 돌아온다. 이건 몇 번 경험하고 나면 꼼꼼함이 귀찮음이 아니라 습관이 된다.

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

커밋은 쿼리 추가, 서비스 레이어 수정, 화면 연동을 각각 독립적으로 쪼갰다. 문제가 생겼을 때 어느 변경에서 깨졌는지 찾기 훨씬 쉬워지고, 리뷰할 때도 변경 의도가 명확하게 보인다. 커밋 메시지는 "무엇을"보다 "왜"를 담으려고 노력한다. add totalMemberCount field보다 대시보드 회원수 노출 - KPI 맥락 기준값 제공이 나중에 git log 볼 때 훨씬 쓸모있다.

댓글 0

첫 댓글 달아줘.