개발 slecs

상품 승인 워크플로우와 공급사 상품 관리 기능 신규 추가

목차

product-approval 영역에 상품 승인 워크플로우와 공급사 상품 관리 기능을 신규 추가했다. 변경 파일은 내부 클래스 2개, SQL 매퍼 2개, 뷰/스타일 2개. 총 6개 파일에 걸친 작업이었다.

기능 명세를 처음 받았을 때 추정한 규모보다 최종 범위가 컸다. "뷰에 컬럼 하나 추가"처럼 보였던 게 결국 쿼리 레벨부터 다시 설계하는 작업으로 커졌다. 이 도메인에서는 자주 있는 일이다.


기존 product-approval 화면은 멀쩡히 돌아가고 있었다. 문제는 운영 중에 반복되는 요청이었다. 공급사 단위로 상품을 묶어서 보고 싶다, 승인 상태별로 흐름을 추적하고 싶다. 처음엔 단순 필터 추가 정도로 볼 수 있었는데, 기존 API가 해당 데이터를 아예 조회하지 않고 있다는 걸 확인하면서 방향이 바뀌었다.

화면에서 필요한 값이 어느 테이블에 있는지, 기존 쿼리가 그 테이블을 이미 조인하고 있는지를 먼저 추적했다. 중간 어딘가 끊겨 있으면 SQL 매퍼부터 다시 설계해야 한다. 이번이 딱 그 케이스였고, 그래서 쿼리 레벨부터 출발해서 클래스, 뷰 순서로 위로 올라오는 방식을 택했다. UI부터 만들고 데이터를 끼워 맞추는 흐름은 정합성 관리가 어렵다.

구현 흐름과 선택

SQL 매퍼 쿼리 구조는 아래와 같이 잡았다. MyBatis 기준이다.

<select id="selectSupplierProductList" parameterType="map" resultType="SupplierProductVO">
  SELECT
    sp.product_id,
    sp.supplier_id,
    s.supplier_name,
    sp.status,
    sp.created_at
  FROM supplier_product sp
  INNER JOIN supplier s ON s.supplier_id = sp.supplier_id
  WHERE sp.status = #{status}
    AND sp.supplier_id = #{supplierId}
  <if test="keyword != null and keyword != ''">
    AND sp.product_name LIKE CONCAT('%', #{keyword}, '%')
  </if>
  ORDER BY sp.created_at DESC
</select>

<if> 분기는 단순해 보여도 파라미터 유무에 따라 실행 계획이 달라질 수 있다. LIKE 조건이 붙으면 인덱스를 타지 않는 경우가 흔하다. 데이터 규모가 지금 당장 크지 않더라도 EXPLAIN 한 번 돌려두는 게 나중에 갑자기 느려지는 상황을 예방한다. 이번에도 미리 확인했다.

내부 클래스 쪽에서는 승인 상태를 상수로 관리하는 방식을 선택했다. 문자열 직접 비교는 오타 하나로 분기 전체가 무너진다. enum을 쓰기 어려운 상황이면 상수 클래스라도 끊어두는 게 맞다.

public class ProductApprovalStatus {
    public static final String PENDING  = "PENDING";
    public static final String APPROVED = "APPROVED";
    public static final String REJECTED = "REJECTED";
}

뷰 연결은 이 다음 단계. 클래스와 매퍼가 정리된 상태에서 렌더링을 붙이면 디버깅 위치가 명확해진다. 화면에서 이상한 값이 나올 때 "쿼리 문제인가, 클래스 변환 문제인가, 렌더링 문제인가"를 분리해서 볼 수 있다.

설계 시 실제로 고민한 것들

구현 자체보다 "어떻게 설계할지"에 시간이 더 걸렸다.

정합성. 같은 상품 ID를 다른 화면에서도 보여주는 케이스가 있어서, 집계 로직이 두 군데로 나뉘면 한 쪽만 수정하고 나머지를 빠뜨리는 사고가 생긴다. 공통 쿼리로 집약하고 뷰에서는 가공만 하도록 정리했다. 금융/결제 도메인에서 숫자가 화면마다 다르게 나오면 신뢰가 흔들린다.

실시간 갱신 여부. 승인 상태 변경은 자주 일어나는 이벤트가 아니다. 폴링이나 웹소켓은 구현 복잡도 대비 실익이 크지 않다고 판단해서 페이지 로드 시 조회 + 수동 새로고침으로 결정했다. 기술을 추가하는 게 능사가 아니고, 안 쓰는 게 나은 케이스가 분명히 있다.

예외 방어. 정상 플로우만 테스트하고 넘어가면 반드시 엣지 케이스에서 터진다. 이번에 사전에 정리한 케이스들은 아래와 같다.

케이스 처리 방식
공급사 상품 0건 빈 리스트 반환, 화면에 안내 문구 표시
status 파라미터 NULL 기본값 PENDING으로 fallback
비인가 사용자 직접 접근 권한 체크 후 403, 로그 기록
LIKE 파라미터 입력값 PreparedStatement 바인딩으로 처리

이 정도를 미리 케이스 테이블로 정리해두면 구현 중 빠뜨리는 항목이 줄어든다.

검증과 개발 습관

구현 후에는 직접 화면에서 동작을 확인했다. 기존 데이터가 깨지지 않았는지, 관련 화면 숫자가 일치하는지 cross-check했다. 방식 자체는 단순하다. 변경 전 화면 상태나 수치를 메모해두고, 배포 후 같은 케이스로 다시 확인한다. 관련 화면이 여러 개면 각각 숫자를 맞춰본다.

커밋은 논리적으로 독립된 단위로 쪼개는 습관을 유지하고 있다. 이번도 쿼리 추가, 클래스 메서드 추가, 뷰 연결을 분리했다. 한 커밋에 다 몰아넣으면 나중에 어느 변경에서 문제가 생겼는지 추적하기 어렵다. 작게 나누면 bisect든 직접 로그 확인이든 훨씬 빠르다. 메시지도 "무엇을" 보다 "왜"를 담으려고 신경 쓴다. 3개월 뒤에 자기가 읽을 로그라고 생각하면 자연히 그렇게 쓰게 된다.


사내 서비스를 만들다 보면 기능 하나가 화면에 버튼 하나 추가하는 것으로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 관리, 예외 처리, 렌더링, 권한 체크가 전부 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나온다. 특히 금융/결제 도메인은 "대충 맞는 것 같다"로 넘어가면 반드시 나중에 다시 돌아온다. 꼼꼼함이 기본값이어야 하는 도메인이고, 그 기준을 낮추지 않는 게 결국 빠른 길이다.

댓글 0

첫 댓글 달아줘.