개발 slecs

수수료 구조 조회 기능 추가

목차

수수료 구조 조회는 언뜻 단순 SELECT처럼 보인다. 근데 막상 손을 대보면 생각보다 고려할 게 많다.

수수료 데이터는 구조적으로 조건이 겹치는 경우가 많다. 적용 유형, 코드 분류, 시작일 같은 값들이 맞물려야 정확한 수치가 나오고, 이 조건이 하나라도 어긋나면 화면에 엉뚱한 값이 뜨거나 아예 빈 화면을 마주하게 된다. 사용자 입장에서 수수료 정보가 잘못 노출되면 신뢰 문제로 직결되기 때문에, 조회 정확도를 챙기는 게 이번 작업의 핵심이었다.

수정 대상은 내부 클래스였다. Spring MVC 구조에서 내부 클래스 혹은 내부 패키지의 처리 클래스가 요청을 받아 서비스를 호출하고, 결과를 모델에 담아 JSP로 넘기는 흐름을 담당하는 경우가 있다. 기능 단위가 작을 때 자주 쓰이는 패턴인데, 로직이 한 파일 안에 묶여 있어 추적하기 편한 반면, 덩치가 커지면 금방 읽기 어려워진다는 단점이 있다. 이번엔 규모가 크지 않았으니 그대로 두되, 로직 흐름을 정리하는 방향으로 접근했다.

요청 바인딩에서 SQL까지

파라미터 바인딩 단계에서 방어 코드를 충분히 짜두지 않으면 SQL 쪽에서 결과가 꼬이기 시작한다. 수수료 관련 조회는 조건 코드가 숫자로 오기도 하고, 빈 문자열로 오기도 하고, 아예 null로 오기도 해서 타입 변환 실패나 필수 값 누락이 예상 외로 잦다.

MyBatis XML에서 이런 케이스는 <if test="..."> 조건으로 처리하는 게 일반적이다.

<select id="selectCommissionStructure" parameterType="map" resultType="CommissionVO">
  SELECT
    COMM_CODE,
    COMM_RATE,
    APPLY_TYPE,
    START_DATE
  FROM TB_COMMISSION_STRUCTURE
  WHERE USE_YN = 'Y'
    <if test="commCode != null and commCode != ''">
      AND COMM_CODE = #{commCode}
    </if>
    <if test="applyType != null and applyType != ''">
      AND APPLY_TYPE = #{applyType}
    </if>
  ORDER BY START_DATE DESC
</select>

이렇게 동적 쿼리로 분기하면 파라미터 조합이 여러 개여도 하나의 쿼리로 처리할 수 있다. 다만 <if> 분기가 많아지면 실행 계획이 매번 달라져서 DB 캐시 효율이 떨어질 수 있다는 게 트레이드오프다. 조회 빈도가 높은 기능이라면 조건 조합 수를 줄이거나 쿼리를 분리하는 것도 방법이다. 이번 기능은 호출 빈도가 크게 높지 않을 것으로 판단해서 동적 쿼리 방식을 유지했다.

결과 매핑도 신경 쓴 부분이다. resultType으로 VO를 직접 매핑할 때 컬럼명과 필드명이 다르면 값이 들어오지 않는다. MyBatis의 camelCase 매핑 설정이 켜져 있으면 COMM_RATEcommRate로 자동 변환되지만, 설정이 꺼져 있으면 resultMap을 별도로 정의해야 한다. 기존 프로젝트 설정에서 mapUnderscoreToCamelCase 여부를 먼저 확인하는 게 순서다.

트랜잭션, 예외, 로그

조회 기능이라도 트랜잭션 범위는 명확히 해야 한다. 읽기 전용 조회는 @Transactional(readOnly = true)로 선언하면 커넥션 풀에서 불필요한 잠금을 피할 수 있고, DB에 따라서는 읽기 전용 복제본으로 라우팅하는 데 활용되기도 한다.

시나리오 트랜잭션 설정 이유
단순 조회 readOnly = true 잠금 오버헤드 제거
조회 후 이력 기록 readOnly = false 쓰기 포함이므로 일반 트랜잭션
트랜잭션 불필요 어노테이션 생략 단순 캐시 조회 등

예외 처리는 RuntimeException 전파 vs 잡아서 응답 변환 중에서 선택해야 한다. 이번엔 서비스 레이어에서 조회 결과가 없을 때 빈 VO를 반환하는 방식을 택했다. null을 그대로 올리면 컨트롤러에서 NPE 가능성이 생기고, JSP에서도 EL 표현식이 조용히 빈 값을 찍어버려서 운영 중 디버깅이 어렵다. 빈 VO를 반환하고 뷰에서 값 유무를 체크하는 편이 흐름이 명확하다.

로그는 운영에서 디버깅 가능하도록 중요 입출력값을 INFO 레벨로 남겨뒀다. 요청 파라미터와 조회 결과 건수 정도면 충분하다. 수수료 금액 자체는 민감할 수 있어서 실제 값 대신 건수만 남기는 것도 고려할 만하다.

log.info("[수수료구조조회] commCode={}, applyType={}, resultSize={}",
    param.getCommCode(), param.getApplyType(), resultList.size());

JSP 뷰 쪽에서는 JSTL <c:choose><c:if>로 빈 결과 케이스를 분기 처리했다. 데이터가 없을 때 아무것도 안 보이는 화면보다, "조회 결과가 없습니다" 한 줄이라도 있는 편이 훨씬 낫다.


배포 순서는 로컬 기동 확인 후 개발 DB 반영, 그다음 배포로 처리했다. 작업 규모가 작아도 이 순서는 지키는 게 맞다. 로컬에서만 확인하고 올리다 보면 개발 DB의 참조 데이터 차이나 설정값 차이에서 나오는 문제를 놓치는 경우가 생긴다.

이런 조회 기능 하나가 시스템 신뢰도에 직접 닿는다는 점에서, 작은 작업이라도 바인딩-SQL-뷰를 전 구간 꼼꼼히 챙기는 습관이 중요하다. 특히 수수료처럼 사용자가 민감하게 보는 데이터일수록 더.

댓글 0

첫 댓글 달아줘.