자동화 slecs

Spring MVC 플랫폼 기능 개선과 무중단 배포 안정성 확보

목차

shorts batch 작업은 규모가 크지 않은 편이다. 커밋 하나 넣고 끝나는 경우도 있고, 이번처럼 내부 클래스 몇 군데를 건드리며 검증 로직 추가하고 배포 흐름 다듬는 정도인 경우도 있다. 그런데 이런 작업이 오히려 까다롭다. 기능 자체가 작으니 검토가 얕아지기 쉽고, "이 정도야 그냥 올리면 되지"라는 판단이 나중에 작은 장애 원인이 되기도 한다.

이번 작업은 크게 두 축이었음. 하나는 실제 사용 흐름에서 빠져있던 기능을 채우는 것, 다른 하나는 배포 방식을 정비하는 것. 둘 다 큰 변경은 아니었지만 손대다 보면 생각할 거리가 생긴다.

내부 클래스 수정에서 꼭 챙기는 것들

Spring MVC + MyBatis 구조에서 컨트롤러 내부 클래스는 요청 바인딩부터 서비스 호출, 뷰 처리까지 한 파일에 모여있는 경우가 많다. 규모가 작은 플랫폼일수록 이 패턴이 자주 보인다. 빠르게 개발할 수 있다는 장점은 있는데, 로직이 길어지면 유지보수가 복잡해짐. 어디에 뭐가 있는지 파악하는 데 시간이 걸리고, 수정 범위 추적도 번거롭다.

이번 수정 포인트는 세 곳이었다.

요청 파라미터 검증: @RequestParam으로 들어오는 값이 null이거나 빈 문자열인 경우를 명시적으로 잡아야 했음. Spring의 @Valid + BindingResult를 쓸 수도 있지만, 이 프로젝트는 DTO 없이 파라미터를 직접 받는 방식이었다. 그래서 컨트롤러 앞단에서 직접 체크했음.

if (StringUtils.isBlank(param)) {
    return ResponseEntity.badRequest().body("필수 파라미터 누락");
}

단순해 보이지만 이 한 줄이 없으면 서비스 레이어까지 null이 흘러내려가서 NPE가 터진다. MyBatis 쿼리에서 WHERE 조건이 통째로 사라지는 경우도 있어서, 파라미터 검증은 입구에서 끊는 게 맞다.

트랜잭션 범위: DB를 건드리는 로직에는 @Transactional 범위를 명확히 잡았다. 이게 모호하면 일부 INSERT만 커밋되고 나머지가 롤백 안 된 채로 남는 상황이 생긴다. MyBatis mapper를 여러 번 호출하는 경우 하나의 트랜잭션 안에 묶어야 할 범위를 서비스 메서드 단위로 정리했음.

상황 처리 방식
단일 mapper 호출 서비스 메서드에 @Transactional
복수 mapper 호출 최상위 서비스 메서드에 묶어서 적용
읽기 전용 조회 @Transactional(readOnly = true)
예외 발생 시 롤백 rollbackFor = Exception.class 명시 여부 확인

트랜잭션 어노테이션은 붙이는 것보다 범위를 어디까지로 볼지가 중요하다. 서비스 메서드 하나에 너무 많은 로직이 묶이면 락 경합 시간이 길어지고, 너무 좁으면 중간에 실패했을 때 데이터가 부분적으로 반영된다.

예외 전파 vs 변환: RuntimeException을 그대로 올릴지, 잡아서 클라이언트 응답으로 바꿀지는 상황마다 다르다. 이번에는 JSP 렌더링과 API 응답이 섞인 구조였음. JSP 흐름이면 예외가 올라가도 공통 에러 페이지로 떨어지면 되는데, API 응답이면 HTTP 상태코드와 바디를 직접 내려줘야 해서 컨트롤러에서 try-catch로 잡아 처리했다. 두 방식이 한 프로젝트에 공존하면 각 흐름마다 예외 처리 정책을 따로 정리해두는 게 나중에 혼선을 줄인다.

로그는 중요 입출력값을 INFO로 남겼음. DEBUG로 두면 운영 환경 로그 레벨에서 안 보이는 경우가 있어서, 진단에 쓰일 값은 INFO 레벨로 박아둔다. 물론 민감한 값은 마스킹하고.

무중단 배포 - 헬스체크가 핵심

작업 자체보다 이번에 더 신경 쓴 건 배포 방식이었다. 작은 변경이라도 서비스 중 재시작이 일어나면 짧은 순간이라도 에러가 나기 때문에, 트래픽 전환 타이밍을 다듬었음.

흐름은 단순하다.

  • 신규 버전 서버 기동
  • 헬스체크 엔드포인트에서 200 확인
  • 프록시 설정 변경으로 트래픽 전환
  • 구버전 서버 정리

2번을 제대로 안 하면 신규 버전이 떠있어도 초기화가 덜 된 상태에서 요청을 받아 에러가 난다. Spring Boot라면 /actuator/health가 기본 제공되지만, 이 프로젝트처럼 레거시 Spring MVC면 직접 만들어야 한다.

@RequestMapping("/health")
@ResponseBody
public String health() {
    return "ok";
}

이 정도면 기본은 된다. DB 연결까지 확인하는 딥 헬스체크가 필요한 경우엔 DataSource 접속 테스트 로직을 추가하면 되는데, 헬스체크 자체가 너무 무거우면 체크하는 과정에서 응답이 느려지는 부작용이 생길 수 있어서 균형을 잡아야 한다.

롤백 준비도 빠뜨리면 안 된다. JAR 배포면 직전 버전 파일을 일정 기간 보관해두고, Docker라면 이전 이미지 태그를 섣불리 삭제하지 말 것. 문제 생겼을 때 "구버전 어디 갔지" 하면서 찾는 동안 장애가 길어지는 건 흔한 패턴이다. 배포 성공 후 최소 하루, 중요한 변경이면 며칠은 이전 버전을 유지하는 게 낫다.

작업하면서 다시 확인한 것들

수정 규모가 작으면 "눈으로 봐도 되겠지"가 되기 쉽다. 이번에도 그런 유혹이 있었는데, 기존 로직과 연결되는 부분은 꼼꼼히 따라갔음. 내부 클래스 수정이 외부에서 호출하는 흐름에 사이드이펙트를 줄 수 있어서, 직접 콜 스택을 추적하며 확인했다.

로컬 기동 확인 → 개발 DB 반영 → 배포. 이 순서는 귀찮아도 건너뛰지 않는 게 맞다. 특히 SQL XML 수정이 포함되면 MyBatis 쿼리 오류는 런타임에야 잡히기 때문에, 개발 DB에서 한 번 실행해보는 과정이 없으면 배포 후에 쿼리 에러를 마주치게 된다.

작은 커밋이라도 같은 체크리스트를 따르는 게 결국 시간을 아낀다. 이 정도 작업이 쌓이면서 전체 플랫폼의 신뢰도가 조금씩 올라간다는 건 변하지 않는 사실이다.

댓글 0

첫 댓글 달아줘.