커미션 조회 화면 안정성
목차
커미션 조회 화면은 생각보다 손이 많이 가는 영역이다. 단순히 "데이터 뽑아서 보여준다"는 것처럼 보이지만, 실제로 운영하다 보면 파라미터 바인딩 실수 하나로 전체 목록이 빠지거나, null 처리를 빠뜨려서 특정 케이스에서 화면이 깨지는 일이 생긴다. 이번 작업은 그런 종류의 개선이었다.
새 기능을 추가하기보다 기존 흐름에서 빠져 있던 안전장치를 채우는 작업. 수정 범위는 내부 클래스 중심이었고, Spring MVC + MyBatis 구조에서 요청 처리부터 SQL 결과 매핑까지 흐르는 경로 전체를 한 번 점검했다.
Spring MVC 내부 클래스 구조에서 주의할 것
MyBatis 기반 프로젝트에서 내부 클래스(Inner Class)를 쓰는 패턴은 흔하다. 파라미터 VO나 결과 DTO를 Controller 또는 Service 안에 static nested class로 두는 방식인데, 짧은 코드로 쓸 때는 편하지만 관리 포인트가 쌓이면 문제가 생긴다.
이번에 건드린 클래스도 그런 구조였다. 요청 파라미터를 받는 VO, 결과를 담는 DTO가 한 파일 안에 nested로 있었고, 바인딩 처리와 비즈니스 로직이 섞여 있었다. 리팩터링 방향은 간단했다.
- 요청 파라미터 검증을 바인딩 시점에 명확히 분리
- 비즈니스 로직은 내부 클래스가 아니라 Service 레이어에서 처리하도록 역할 정리
- SQL XML의 쿼리와 결과 매핑 타입을 DTO 필드명과 맞춤
특히 MyBatis에서 결과 매핑 미스는 디버그가 까다롭다. 컬럼명과 필드명이 맞지 않으면 조용히 null로 떨어지는데, 개발 환경에서는 그냥 지나치다가 운영 데이터 붙이면 갑자기 화면이 비는 케이스가 나온다.
<!-- 컬럼명 - 필드명 불일치 예시 -->
<resultMap id="commissionResult" type="CommissionDto">
<result column="comm_amt" property="commAmt" />
<result column="apply_dt" property="applyDt" />
<result column="status_cd" property="statusCd" />
</resultMap>
resultMap을 명시적으로 쓰는 게 번거롭긴 해도, camelCase 자동 변환에 의존하다가 예외 케이스 만나면 더 고생한다. 이번에 미처 매핑 안 된 컬럼이 하나 있었고, 그걸 찾는 데 시간을 좀 썼다.
트랜잭션 범위와 예외 처리 결정
조회 화면이라 트랜잭션이 크게 문제되진 않는다고 생각하기 쉬운데, 조회 후 특정 상태를 업데이트하는 흐름이 한 묶음에 있으면 얘기가 다르다. 이번 케이스도 커미션 조회 후 특정 조건에서 상태 플래그를 바꾸는 로직이 있었다.
트랜잭션 범위를 어디까지 잡을지는 항상 판단이 필요하다.
| 케이스 | 트랜잭션 범위 | 이유 |
|---|---|---|
| 단순 조회 | 없음 또는 readOnly=true |
커넥션 반환 빠르게, 락 없음 |
| 조회 + 상태 변경 | @Transactional 묶음 |
일관성 보장 |
| 외부 API 호출 포함 | 신중하게, 가능하면 분리 | 타임아웃 시 롤백 범위 문제 |
이번엔 조회 + 상태 변경이 있었으므로 Service 메서드 단위로 @Transactional을 걸었다. 범위를 넓게 잡는 게 아니라 변경이 일어나는 메서드만 정확히 지정하는 방식.
예외 처리도 결정이 필요한 부분이었다. RuntimeException을 그냥 위로 전파할 건지, 잡아서 응답 객체로 변환할 건지. 이번엔 사용자가 직접 보는 화면 요청이라 잡아서 에러 메시지를 응답에 담는 방향으로 처리했다. 전파시키면 공통 ExceptionHandler에서 처리되긴 하는데, 커미션 조회 특성상 부분 실패를 더 친절하게 안내하는 게 나았다.
try {
result = commissionService.getOverview(param);
} catch (CommissionException e) {
log.warn("[커미션 조회] 처리 실패 - param={}, msg={}", param, e.getMessage());
model.addAttribute("errorMsg", e.getMessage());
return "commission/overview-error";
}
로그는 WARN으로 남겼다. ERROR는 아직 시스템이 죽은 게 아니고, DEBUG는 운영에서 안 보이니까. WARN에 파라미터까지 찍어두는 게 나중에 운영 이슈 추적할 때 제일 유용하다.
이런 작은 수정에서 놓치기 쉬운 것들
작업 규모가 크지 않을수록 오히려 꼼꼼히 봐야 하는 이유가 있다. 큰 PR은 리뷰어가 긴장하고 보는데, 작은 수정은 "별거 아니겠지" 하고 슥 넘어가다가 엣지 케이스 하나가 운영에서 뒤늦게 나온다.
이번에도 로컬에서 기동 확인 후 개발 DB에 반영하고 배포하는 순서를 지켰는데, 개발 DB에서 실제 데이터를 붙여보니 resultMap 미스가 하나 더 나왔다. 로컬에선 테스트 데이터만 써서 안 보였던 케이스였다. 실데이터 붙이기 전까지 안 터지는 버그가 이런 식으로 숨어 있다.
수정한 로직이 기존 흐름에 영향 없는지 추적하는 것도 코드 리뷰 때 한 번 더 챙겼다. 혼자 짜고 혼자 보면 빠진 케이스 놓치기 쉬운데, 눈 한 번 더 대는 것만으로도 "이 파라미터가 null일 때 어떻게 돼요?"가 나온다. 실제로 이번에 null 입력 케이스 하나가 그 과정에서 잡혔다.
커미션 조회처럼 매일 쓰는 화면의 안정성은 기능 개발보다 덜 눈에 띈다. 잘 되고 있을 땐 아무도 언급 안 하다가 한 번 깨지면 바로 티가 나는 영역. 화면이 안 깨지고 숫자가 맞게 나오는 그 조용한 신뢰를 유지하는 게 이런 작업의 목적이고, 작은 수정들이 쌓여서 그 신뢰를 만든다.
댓글 0
첫 댓글 달아줘.