파트너 포탈 하위파트너 계약·신청 관리 기능 신설
목차
partner-portal에 하위파트너 계약·신청 관리 기능을 새로 달았다. 변경한 파일은 SQL 매퍼 2개, JSP 뷰 2개와 스타일시트인데, 파일 수만 보면 소소해 보이지만 실제로는 쿼리 설계부터 권한 레이어까지 전 계층을 훑는 작업이었음.
왜 쿼리부터 건드렸나
기존 partner-portal 화면은 상위 파트너 정보 위주로 설계된 구조였다. 하위파트너 계약 상태나 신청 이력 같은 집계 데이터는 애초에 쿼리 자체가 없었음. 단순히 프론트에 테이블 하나 그리는 문제가 아니라, 어떤 데이터를 어떤 기준으로 묶어서 내려줄지를 SQL 레벨부터 설계해야 했다.
금융·결제 도메인에서 집계 쿼리를 잘못 짜면 화면마다 숫자가 달라지는 현상이 생긴다. 신청 건수가 목록에서는 12건인데 상세 합산에서는 11건으로 나오면, 사용자 입장에서는 시스템 자체를 못 믿게 된다. 이걸 방지하려면 집계 기준점, 즉 조회 범위·NULL 처리 방식·상태 코드 필터를 API 전체에서 통일해야 함.
매퍼 작성 시 신경 쓴 부분을 정리하면 이렇다.
- GROUP BY 기준을 응용 로직이 아닌 SQL 레벨에서 확정
- NULL이 들어올 수 있는 컬럼은 COALESCE로 기본값 명시
- 상태 코드 필터는 상수로 분리해서 두 쿼리에서 조건이 다르게 해석되는 것 방지
- 실행 계획(EXPLAIN) 한 번 돌려서 풀스캔이 없는지 확인
-- 하위파트너 계약 현황 집계 (단순화 예시)
SELECT
p.partner_id,
p.partner_nm,
COUNT(c.contract_id) AS total_cnt,
SUM(CASE WHEN c.status_cd = 'ACTIVE' THEN 1 ELSE 0 END) AS active_cnt,
COALESCE(SUM(c.contract_amt), 0) AS total_amt
FROM tb_partner p
LEFT JOIN tb_sub_contract c
ON c.parent_partner_id = p.partner_id
AND c.del_yn = 'N'
WHERE p.partner_type = 'SUB'
GROUP BY p.partner_id, p.partner_nm
LEFT JOIN을 선택한 이유는 계약이 한 건도 없는 하위파트너도 목록에 나와야 했기 때문이다. INNER JOIN으로 짜면 데이터 없는 파트너가 아예 사라져서 등록 직후 화면에서 찾을 수 없는 버그가 생긴다. 집계 결과가 0건인 것과 목록에 아예 없는 것은 완전히 다른 의미이기 때문에, LEFT JOIN으로 행을 살려두고 COUNT/SUM이 0이나 NULL로 내려오도록 처리하는 쪽이 정확하다.
구현 흐름 - 백엔드부터 화면까지
백엔드 API 엔드포인트를 먼저 만들고, 권한 인터셉터 통과 여부를 확인한 다음 프론트를 연동하는 순서로 갔다. 인터셉터를 건너뛰고 화면 먼저 만들면 나중에 권한 예외가 튀어나왔을 때 원인 추적이 복잡해지기 때문임. 순서를 지키는 게 귀찮아 보이지만 디버깅 시간을 오히려 줄여준다.
JSP + AJAX 구조다 보니 비동기 요청 실패 처리도 명시적으로 넣었다. 에러 응답을 그냥 흘리면 화면이 빈 채로 멈추는데, 사용자 입장에서는 로딩인지 에러인지 알 수가 없다.
$.ajax({
url: '/partner/sub/contractList.do',
type: 'GET',
data: { partnerId: partnerId },
success: function(res) {
if (res.resultCode === '00') {
renderContractTable(res.data);
} else {
showErrorMsg(res.resultMsg);
}
},
error: function(xhr) {
showErrorMsg('일시적인 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.');
}
});
사이드바 메뉴 활성화도 빠뜨리기 쉬운 부분이다. URL 패턴이 기존 메뉴와 겹치지 않으면 어느 메뉴에도 active 클래스가 안 붙어서 사용자가 현재 위치를 잃는다. 이번엔 사이드바 활성화 로직이 URL prefix 매칭 방식이라 신규 경로를 추가하고 직접 확인했음.
실시간 갱신 여부도 결정해야 했다. 결론은 폴링 없이 단순 조회로 마무리했다. 계약·신청 데이터는 관리자가 직접 처리하는 흐름이라 실시간성보다 정확성이 우선이고, 굳이 폴링을 붙이면 서버 부하만 늘어남. 이벤트 기반 갱신이 필요하다고 판단될 때 그때 고도화하면 된다. 모든 화면에 실시간성을 달아두는 건 오버 엔지니어링에 가깝다.
검증과 커밋 전략
구현이 끝난 뒤에는 직접 화면을 돌렸다. 확인한 항목은 크게 세 가지였음.
| 확인 항목 | 방법 |
|---|---|
| 기존 데이터 깨짐 여부 | 변경 전 수치를 메모해두고 구현 후 재조회 비교 |
| 관련 화면 숫자 일치 | 목록 집계 - 상세 합산 - 대시보드 요약 값 cross-check |
| 권한 예외 케이스 | 하위파트너 계정으로 직접 접근 시도해 인터셉터 차단 확인 |
변경 전에 현재 상태를 메모해두는 습관이 생각보다 중요하다. 수정 후에 "원래 이랬던가?" 하는 상황이 생기면 비교 기준이 없어서 검증 자체가 흐릿해진다. 스크린샷이든 수치 메모든 기준점을 남겨두는 게 맞다.
커밋은 논리적으로 독립된 단위로 쪼갰다. SQL 매퍼 변경, API 레이어, 뷰 연동을 한 커밋에 몰아넣으면 나중에 어느 변경이 문제를 일으켰는지 추적하기가 어렵다. 작은 커밋을 자주 하면 git bisect나 단순 log 탐색만으로도 어느 지점에서 숫자가 바뀌었는지 금방 찾을 수 있다.
커밋 메시지도 "무엇을 바꿨다"보다 "왜 바꿨는지"를 담으려고 했음. "하위파트너 계약 집계 쿼리 추가"보다 "계약 없는 파트너도 목록에 포함되도록 LEFT JOIN으로 변경"이 나중에 diff를 보는 사람한테 훨씬 유용하다. 특히 팀이 커질수록 커밋 메시지는 코드보다 오래 읽히는 문서가 된다.
사내 서비스를 만들다 보면 기능 하나가 단순히 화면에 버튼 하나 추가하는 것으로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 흐름, 예외 처리, 화면 렌더링, 권한 체크가 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나타남. 금융 도메인에서 "대충 맞는 것 같다" 싶을 때가 제일 위험하다. 그 순간을 넘기면 반드시 다시 돌아온다.
댓글 0
첫 댓글 달아줘.