개발 slecs

수수료 명세 화면 기능 개선

목차

수수료 명세 화면은 사용자 입장에서 "내가 얼마를 왜 냈는지" 확인하는 흐름이라, 표시되는 값이 하나라도 어긋나면 바로 문의가 들어온다. 그래서 작은 개선이라도 꼼꼼하게 챙겨야 하는 영역이다. 이번 작업은 규모 자체는 크지 않았지만, 운영 중에 발견된 엣지 케이스를 잡고 표시 로직을 조금 더 견고하게 다듬는 게 목적이었다.

Spring MVC + MyBatis 구조에서 내부 클래스의 역할

우리 플랫폼은 Spring MVC + MyBatis 조합을 쓴다. 컨트롤러에 붙어 있는 내부 클래스가 요청 파라미터 바인딩과 화면 제어 흐름을 담당하고, 실제 SQL은 XML에서 관리한다. JSP가 뷰 레이어다. 오래된 구조처럼 보이지만, 이 구조에서 내부 클래스를 잘 쓰면 관련 로직이 한 파일에 모여 있어서 컨텍스트 전환 비용이 줄어든다는 장점이 있다.

이번에 수정한 내부 클래스도 그런 역할이었다. 요청이 들어오면 파라미터를 검증하고 바인딩한 다음, 비즈니스 로직을 처리해서 MyBatis 매퍼로 SQL을 호출하고, 결과를 JSP에 넘기거나 API 응답으로 구성하는 흐름. 딱 그 라인 안에서 움직인다.

문제는 파라미터 검증 단계에서 null이나 빈 문자열이 그대로 쿼리까지 흘러들어가는 경우를 완전히 막지 못하고 있었다는 거다. MyBatis XML에서 <if test="param != null and param != ''"> 조건을 쓰기는 했는데, 일부 파라미터는 조건 없이 그냥 들어가고 있었다. 운영 데이터에서 특정 조건일 때 결과가 이상하게 나온다는 걸 확인하고 파고들어 보니 거기서 걸렸다.

수정 방향은 두 가지였다.

  • 컨트롤러 진입 시점에서 필수 파라미터 존재 여부 먼저 체크하고, 이상하면 일찍 리턴
  • SQL XML 쪽에서도 동적 조건을 명시적으로 추가해서 방어층을 두 겹으로
<!-- 수정 전 -->
<select id="selectFeeDetail" resultType="FeeDetailVO">
  SELECT fee_id, amount, fee_type
  FROM fee_detail
  WHERE user_id = #{userId}
  AND fee_date BETWEEN #{startDate} AND #{endDate}
</select>

<!-- 수정 후 -->
<select id="selectFeeDetail" resultType="FeeDetailVO">
  SELECT fee_id, amount, fee_type
  FROM fee_detail
  <where>
    <if test="userId != null and userId != ''">
      AND user_id = #{userId}
    </if>
    <if test="startDate != null and endDate != null">
      AND fee_date BETWEEN #{startDate} AND #{endDate}
    </if>
  </where>
</select>

<where> 태그를 쓰면 조건이 하나도 안 붙는 경우 WHERE 절 자체가 생략되고, 첫 번째 AND도 자동으로 처리된다. 이걸 안 쓰고 직접 WHERE 1=1 AND ... 방식으로 짜는 코드가 아직도 꽤 남아있는데, MyBatis에서 제공하는 태그를 쓰는 게 의도가 명확하고 나중에 조건 추가할 때도 덜 실수한다.

트랜잭션 범위와 예외 처리 판단

조회 기능이라 트랜잭션 범위 자체는 단순했다. 읽기 전용이면 @Transactional(readOnly = true)를 명시하는 게 맞고, 실제로 이번에 빠져있던 걸 추가했다. readOnly 옵션이 성능에 직접적으로 영향을 주는 경우는 DB 드라이버나 리플리카 분리 설정에 따라 다르지만, 의도를 코드에 명시해두는 것 자체로 가치가 있다. "이 메서드는 절대 쓰기 안 함"이라는 신호를 다음 사람에게 줄 수 있으니까.

예외 처리 쪽은 판단이 필요한 부분이 있었다. RuntimeException을 그냥 위로 전파할지, 잡아서 응답 객체로 변환할지. 이번 화면은 JSP 렌더링 흐름이라 예외가 터지면 500 에러 페이지로 빠지는 구조였다. 사용자 입장에서는 그게 가장 나쁜 경험이라서, 명세 데이터를 못 가져오는 경우에는 빈 목록과 함께 안내 메시지를 내려주는 쪽으로 처리했다.

예외 상황 기존 처리 변경 후 처리
필수 파라미터 누락 쿼리까지 흘러가 결과 이상 진입 시점에서 조기 리턴, 안내 메시지
DB 조회 실패 500 전파 빈 목록 응답, 로그 ERROR 기록
날짜 범위 비정상 조건 무시하고 전체 조회 검증 후 범위 보정 또는 거부

이런 판단을 할 때 기준은 하나다. 사용자가 보는 화면에서 예외가 "불투명하게" 처리되면 안 된다. 에러든 빈 결과든, 왜 데이터가 없는지 알 수 있게 해야 한다. 운영자 입장에서도 로그에 충분한 컨텍스트가 남아야 디버깅이 된다.

배포까지 가는 흐름과 CI 환경에서 자주 걸리는 것들

로컬에서 기동 확인하고 개발 DB 반영한 다음 배포하는 순서는 늘 같다. 근데 CI/CD 파이프라인에서 가끔 발목을 잡히는 지점들이 있어서 적어둔다.

SSH 타임아웃은 빌드가 길어지면 연결이 끊기는 문제다. Gradle 빌드 중에 태스크가 오래 걸리면 원격 서버 세션이 먼저 죽는 경우가 있어서 command_timeout을 명시해야 한다. 키 인증 오류는 개행 문자 문제인 경우가 많다. PEM 키를 복사할 때 줄바꿈이 빠지거나 CRLF로 바뀌면 인증이 안 된다. Gradle 캐시가 CI 환경에서 꼬이는 건 --no-daemon 옵션이나 캐시 디렉터리 클리어로 해결할 때가 많다. 환경변수 누락은 배포 자체는 성공해도 앱이 안 뜨는 케이스라 제일 찾기 귀찮다. Secrets 설정 한 줄 빠뜨린 게 원인인데 배포 로그에는 아무 에러가 없으니까.

이번 작업 자체는 빠르게 마무리됐다. 코드 변경량은 적었지만, 파라미터 검증 구멍을 막고 예외 흐름을 정리한 게 운영 안정성에는 실질적인 차이를 만든다. 수수료 명세처럼 민감한 화면일수록 "잘 되는 것"보다 "이상한 값이 절대 나오지 않는 것"이 더 중요하다. 그 기준으로 보면 이번 변경은 충분히 의미 있는 작업이었다.

댓글 0

첫 댓글 달아줘.