개발 slecs

파트너 포탈에 공급사 브랜드·상품 업로드·KPI 대시보드 기능 추가

목차

partner-portal 영역에 공급사 브랜드 관리, 상품 업로드 UX, KPI 대시보드 확장 세 가지를 한 스프린트에 묶어서 올렸다. 내부 클래스 4개, SQL 파일 1개, SQL 매퍼 1개. 숫자만 보면 작은 작업인데, 실제로는 쿼리 설계부터 다시 들어갔다.

기존 포탈은 공급사가 자기 브랜드 정보를 직접 관리할 수 없었고, 상품 업로드도 어드민을 거쳐야 했다. KPI 화면은 있었지만 집계 기준이 모호해서 다른 화면 숫자와 자주 안 맞았다. 세 문제가 사실 같은 뿌리인데, 데이터 주체(공급사)가 자기 데이터를 직접 핸들링하는 흐름이 없었던 거다.

쿼리부터 다시 짠 이유

KPI 숫자가 화면마다 다르게 보이는 문제는 UI 수정으로 해결할 수 없다. 집계 로직이 분산돼 있었고, 어떤 화면은 SUM을 Java 레이어에서 돌리고 어떤 화면은 SQL에서 돌리고 있었다. 단일 집계 쿼리를 SQL 파일에 정의하고, 매퍼에서 DTO로 매핑하는 구조로 바꿨다.

-- 기존: Java에서 루프 돌며 합산
-- 변경 후: SQL 집계 한 방에
SELECT
    s.supplier_id,
    COUNT(DISTINCT p.product_id)      AS product_cnt,
    SUM(o.order_amount)               AS total_amount,
    SUM(CASE WHEN o.status = 'CANCEL' 
             THEN o.order_amount ELSE 0 END) AS cancel_amount
FROM supplier s
LEFT JOIN product p ON p.supplier_id = s.supplier_id
LEFT JOIN orders  o ON o.supplier_id = s.supplier_id
                   AND o.order_date BETWEEN #{startDate} AND #{endDate}
WHERE s.supplier_id = #{supplierId}
GROUP BY s.supplier_id

LEFT JOIN을 쓴 건 주문이 없는 공급사도 KPI 화면에 나와야 해서다. INNER로 하면 신규 공급사가 화면에서 아예 사라지는 문제가 생긴다. 이런 케이스가 실제로 있었고, 기존 코드가 그 방식이었다.

매퍼도 resultMap 정의를 명시적으로 다시 썼다. autoMapping에 의존하면 컬럼이 추가될 때 DTO 필드랑 슬쩍 어긋나는 버그가 조용히 생긴다. 금융/결제 도메인에서 조용한 버그는 제일 나쁜 종류다.

기능별 설계 판단

세 기능 각각 결정해야 할 지점이 달랐다.

기능 주요 판단 선택
브랜드 관리 로고 업로드 실시간 반영 vs 캐시 저장 후 redirect, 캐시 무효화
상품 업로드 대량 업로드 배치 vs 건별 AJAX 일단 건별, 배치는 다음 이터레이션
KPI 대시보드 폴링 vs 이벤트 갱신 폴링 (30초), 실시간 요건 없음

상품 업로드는 배치로 갔으면 더 완성도가 높았겠지만, 이번 요건에서 공급사당 업로드량이 많지 않아서 건별 AJAX로 충분했다. 대량 업로드가 필요해지면 그때 파일 파싱 + 비동기 처리 구조로 교체하면 된다. 지금 단계에서 오버엔지니어링 하는 건 유지보수 비용만 올린다.

권한 인터셉터는 기존 패턴 그대로 탔다. 공급사 롤이 포탈 메뉴에 접근하는 경로가 이미 정의돼 있었고, 새 엔드포인트들을 거기에 등록하는 방식. 인터셉터를 우회하거나 별도 필터를 만드는 건 나중에 보안 감사할 때 구멍이 된다. 기존 흐름에 얹는 게 맞다.

JSP + AJAX 방식은 레거시 스택이긴 하지만, 이 프로젝트의 나머지 화면들이 다 그 방식이라 혼자 React 컴포넌트 올리는 건 오히려 이질적이다. 일관성이 먼저다.

검증하면서 발견한 것들

구현 후에 직접 화면에서 돌려보는 건 당연한 수순인데, 이번에 특히 신경 쓴 건 기존 화면이랑 숫자 cross-check이었다.

KPI 집계 쿼리를 바꾼 이후에, 기존 어드민 화면의 합산값과 포탈 대시보드값이 일치하는지 같은 날짜 범위로 비교해봤다. 처음엔 맞지 않았다. 원인은 어드민 쿼리가 order_date를 타임스탬프 기준으로 자르고, 새 쿼리는 날짜 문자열 비교를 쓰고 있어서 경계값에서 1건이 빠졌다. 자잘해 보이지만 이게 누적되면 정산 오류다.

이런 류의 차이는 코드 리뷰에서 잘 안 잡힌다. 로직이 맞아 보이니까. 실제 데이터로 돌려봐야 나온다.

NULL 처리도 챙겼다. 신규 공급사는 브랜드 정보가 없어서 브랜드 관련 필드가 NULL인데, JSP에서 JSTL 없이 EL 표현식으로 찍으면 'null' 문자열이 그대로 출력된다. DTO 초기화 단계에서 빈 문자열로 내려주는 게 맞는데, 매퍼 resultMap에 nullValue 지정으로 처리했다.


사내 서비스, 특히 포탈처럼 여러 역할이 같은 데이터를 다른 각도로 보는 시스템은 "기능 추가 = 새 화면 하나"가 아니다. SQL 집계 기준, 상태 정의, 권한 경계, 예외 케이스가 전부 엮인다. 클래스 4개, SQL 2개라는 변경 규모가 작아 보여도 실제로 검토하고 결정한 항목은 그보다 훨씬 많다.

커밋은 브랜드 관리 / 상품 업로드 / KPI 쿼리 / 매퍼 분리해서 4개로 쪼갰다. 나중에 KPI 숫자 이슈가 생기면 쿼리 커밋만 따라가면 되게. 커밋 쪼개는 습관이 귀찮아 보여도, 문제 터졌을 때 git bisect 범위가 반으로 줄어드는 효과가 있다. 그게 결국 장애 대응 시간이다.

댓글 0

첫 댓글 달아줘.