개발 slecs

JSP 화면과 내부 클래스 로직 개선

목차

Spring MVC + MyBatis 기반 플랫폼에서 내부 클래스 구조를 손보고 JSP 화면 흐름을 개선한 작업이다. 기능 자체가 새로 생긴 건 아니고, 운영 중 발견된 개선 포인트를 반영한 것. 규모는 크지 않았지만 손 안 대면 언젠가 터질 것들이었다.

내부 클래스와 요청 바인딩 구조

Spring MVC에서 컨트롤러 내부에 static inner class로 요청/응답 모델을 두는 패턴은 꽤 흔하다. 파일 분리 없이 해당 엔드포인트에 필요한 데이터 구조를 한 곳에서 볼 수 있다는 장점이 있다. 반대로 클래스가 커지면 가독성이 급격히 떨어지고, 같은 DTO를 다른 곳에서 재사용하기 어렵다는 단점도 생긴다.

이번 작업에서 내부 클래스가 맡은 역할은 크게 두 가지였다. 요청 파라미터를 받아서 바인딩·검증하는 부분, 그리고 바인딩된 값으로 비즈니스 로직을 처리하는 부분. 이 두 책임이 한 클래스에 섞여 있으면 테스트도 어렵고 나중에 로직만 따로 뜯어내기도 힘들어진다. 이번엔 완전히 분리하진 않았지만, 최소한 메서드 단위로 역할을 명확히 나눠놓는 쪽으로 정리했다.

요청 바인딩에서 항상 챙겨야 하는 건 null과 빈 값 처리다. 프론트에서 보내지 않는 파라미터가 null로 들어오는 건지, 빈 문자열로 들어오는 건지, 아예 키 자체가 없는 건지 - 이 세 경우를 다 다르게 처리해야 할 때가 있다. MyBatis XML에서도 아래 패턴을 쓰는 이유가 거기 있음.

<!-- 검색 조건이 있을 때만 WHERE 절 추가 -->
<select id="selectList" parameterType="SearchParam" resultType="ResultDto">
  SELECT *
  FROM target_table
  <where>
    <if test="keyword != null and keyword != ''">
      AND name LIKE CONCAT('%', #{keyword}, '%')
    </if>
    <if test="status != null">
      AND status = #{status}
    </if>
  </where>
  ORDER BY created_at DESC
</select>

<where> 태그를 쓰면 첫 번째 조건 앞에 AND가 붙는 문제를 자동으로 처리해준다. trim 태그로 직접 다듬는 것보다 의도가 명확하게 드러나서 선호하는 편이다.

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

이번 작업에서 가장 신경 쓴 건 트랜잭션 범위였다. @Transactional을 어디에 걸지, 얼마나 좁게 걸지 결정하는 게 생각보다 까다로웠다.

원칙은 단순하다 - DB를 변경하는 작업 단위가 원자적으로 처리되어야 하면 그 단위에 트랜잭션을 건다. 근데 실제로는 "이 서비스 메서드가 다른 서비스를 내부에서 호출하는데 거기도 @Transactional이 붙어있다"는 상황이 자주 생기고, Spring의 전파 옵션을 모르면 의도치 않게 트랜잭션이 합쳐지거나 분리된다.

전파 옵션 동작
REQUIRED (기본) 기존 트랜잭션 있으면 합류, 없으면 새로 생성
REQUIRES_NEW 항상 새 트랜잭션 생성, 기존은 일시 중단
NOT_SUPPORTED 트랜잭션 없이 실행, 기존은 중단
NESTED 기존 안에서 savepoint 기반 중첩

이번엔 REQUIRED 기본값으로 처리했다. 로직이 복잡하지 않았고 호출 체인도 단순해서 REQUIRES_NEW 같은 분리가 필요한 케이스가 아니었다.

예외 처리는 RuntimeException 전파 vs 잡아서 응답으로 변환하는 두 가지 선택지 사이에서 항상 고민이 생긴다. 내부 서비스 레이어에서는 RuntimeException을 그냥 올려보내고, 컨트롤러 레이어나 @ControllerAdvice에서 한꺼번에 잡아 처리하는 패턴이 코드를 깔끔하게 유지하는 데 더 낫다는 걸 이번에도 확인했다. try-catch를 서비스 중간중간에 뿌려두면 어디서 뭘 잡고 있는지 추적이 안 됨.

로그는 INFO 레벨로 입출력 핵심값만 남겼다. DEBUG는 운영 환경에서 보통 꺼져 있어서, 실제 이슈가 터졌을 때 DEBUG 로그를 못 보는 경우가 많다. 서비스 메서드 진입 시 주요 파라미터, 결과 반환 직전 핵심 응답값 정도는 INFO로 박아두는 편이다.

배포 순서와 롤백 준비

로컬 기동 확인 → 개발 DB 반영 → 배포 순서로 처리했다. 작은 수정이라도 이 순서를 지키는 게 맞다. 로컬에서만 확인하고 바로 운영 배포했다가 개발 DB 스키마 차이로 터지는 경우는 실제로 생긴다.

무중단 배포는 신규 버전을 완전히 기동시킨 뒤 프록시 설정을 변경해서 트래픽을 전환하는 방식으로 처리했다. 이때 헬스체크 엔드포인트가 없으면 문제가 생긴다. 프로세스가 올라와 있어도 스프링 컨텍스트 로딩이 끝나지 않은 상태에서 트래픽이 들어오면 에러가 난다.

// 레거시 Spring MVC 환경에서의 간단한 헬스체크
@GetMapping("/health")
@ResponseBody
public ResponseEntity<String> health() {
    return ResponseEntity.ok("ok");
}

Spring Boot라면 Actuator의 /actuator/health를 그냥 쓰면 되지만, 레거시 Spring MVC 환경에서는 이런 단순한 엔드포인트를 직접 만들어두는 게 현실적이다. 뭐라도 있어야 배포 스크립트에서 기동 완료 여부를 판단할 수 있다.

롤백 준비는 배포 전략의 일부다. 이전 버전 JAR를 지우지 않고 남겨두거나, Docker 환경이라면 이미지 태그를 유지해두는 것. "문제 없을 거야"라는 확신 아래 이전 버전을 지우고 배포했다가 롤백이 안 되는 상황은 최악이다. 복구에 드는 시간보다 롤백 준비 비용이 훨씬 싸다.

이런 작은 수정들이 쌓이면 코드베이스의 맥락이 생기고, 나중에 더 큰 리팩토링을 할 때 기준점이 된다. 당장 눈에 띄는 성과가 없어 보여도 정리해두는 게 맞다.

댓글 0

첫 댓글 달아줘.