개발 slecs

개발 DB 반영과 무중단 배포 안정성 개선

목차

Spring MVC + MyBatis 구조에서 내부 클래스를 손보는 작업은 규모 자체는 크지 않다. 컨트롤러 하나, SQL XML 한 조각, JSP 한 장면 정도. 그런데 이런 작업이 오히려 더 까다롭다. 건드리는 범위가 좁을수록 "이 정도는 괜찮겠지"라는 방심이 생기고, 그게 기존 흐름에 조용히 영향을 주는 경우가 종종 있었기 때문이다.

이번 작업도 그런 케이스였다. 실 사용자 흐름에서 필요한 기능이었거나 운영 중 발견된 개선 포인트를 반영하는 작업이었는데, 내부 클래스 수정이라 영향 범위 파악부터 시작했다.

내부 클래스와 SQL, 어디를 얼마나 건드렸나

Spring MVC에서 컨트롤러 내부 클래스는 요청 파라미터를 바인딩하고, 서비스 레이어로 넘기기 전에 검증을 처리하는 역할을 담당한다. MyBatis SQL XML과 결과 매핑이 따라붙으니까, 수정 범위는 자연스럽게 세 군데로 퍼진다.

  • 요청 파라미터 바인딩 및 검증 (컨트롤러 레이어)
  • 비즈니스 로직 처리 (서비스 또는 내부 클래스)
  • SQL 쿼리 작성 및 resultMap 매핑 (MyBatis XML)
  • JSP 또는 API 응답 구성

이중에 가장 신경 쓴 건 바인딩과 매핑 쪽이었다. null이나 빈 문자열이 들어왔을 때 그냥 통과시킬 건지, 아니면 400으로 튕길 건지. 이걸 명확히 안 해두면 프론트에서 "왜 가끔 에러 나요?"라는 문의가 들어온다.

예외 처리는 이번에도 고민이 됐다. RuntimeException을 그냥 올려보내면 스프링의 @ControllerAdvice에서 잡힐 텐데, 그냥 올리는 게 나은 경우도 있고 서비스 레이어에서 의미 있는 응답으로 변환하는 게 나은 경우도 있다. 기준은 결국 "이 예외가 비즈니스 예외냐, 아니냐"다.

// 비즈니스 예외는 변환해서 응답
try {
    result = someService.process(param);
} catch (IllegalArgumentException e) {
    log.warn("invalid param: {}", param, e);
    return ResponseEntity.badRequest().body("잘못된 요청입니다.");
}

// 시스템 예외는 그냥 올려보내서 ControllerAdvice 처리
// → 500으로 통일, 운영 로그에서 추적

트랜잭션 범위는 짧게 가져가는 편을 선호한다. DB를 건드리는 로직만 @Transactional 안에 두고, 외부 I/O나 로그 같은 건 밖으로 뺀다. 트랜잭션이 길수록 커넥션 점유 시간이 늘어나고, 운영 중 커넥션 풀 고갈로 이어질 수 있다. 특히 배치성 작업이 섞여 있을 때 이 경계가 흐릿하면 나중에 디버깅이 굉장히 힘들어진다.

로그는 INFO 레벨로 중요 입출력값을 남겨뒀다. 운영 서버에서 DEBUG를 켜기 어려운 환경이면 INFO 로그가 유일한 디버깅 수단이 된다. "요청 파라미터 전체를 찍는 건 보안 이슈 아니냐"는 질문이 있는데, 맞는 말이긴 하다. 민감 필드는 마스킹하거나 아예 제외하고, 식별에 필요한 ID나 상태값 정도만 찍는 게 균형점이다.

개발 DB 반영부터 배포까지 순서

이번 작업 흐름은 단순했다. 로컬 기동 확인 → 개발 DB 반영 → 배포. 근데 이 단순한 순서가 흐트러지면 꽤 번거로운 일이 생긴다.

개발 DB에 반영하기 전에 로컬에서 충분히 검증하지 않으면, 개발 DB가 테스트 오염 상태가 되어 다른 개발자의 작업과 충돌하는 경우가 생긴다. 공유 개발 DB를 쓰는 환경에서는 특히 그렇다. 그래서 로컬에서 단위 케이스를 다 확인하고, 개발 DB에는 최종 상태만 반영하는 식으로 관리했다.

MyBatis XML 수정 시 자주 하는 실수가 있다. resultMap의 컬럼명과 실제 DB 컬럼명이 안 맞는 케이스. 로컬 H2나 인메모리 DB에서는 대소문자 무시해서 통과되는데, 운영 DB(MySQL 등)에서는 케이스 센시티브하게 동작하는 경우가 있어서 배포 후에 문제가 터지기도 한다. 개발 DB에서 한 번 더 확인하는 이유 중 하나다.

<!-- resultMap 컬럼 케이스 주의 -->
<resultMap id="SomeResultMap" type="SomeVO">
    <result property="userId"   column="user_id" />
    <result property="userName" column="user_name" />
    <!-- 컬럼명은 DB 실제 컬럼 기준, 대소문자 그대로 -->
</resultMap>

무중단 배포, 타이밍이 전부다

배포 자체는 자동화가 되어 있더라도 타이밍은 여전히 사람이 신경 써야 하는 부분이 있다.

신규 버전 기동 완료 전에 프록시 설정을 바꾸면 그 사이에 들어오는 요청이 에러를 맞는다. 그래서 배포 순서는 항상 이렇게 잡는다.

단계 작업 확인 기준
1 신규 버전 기동 애플리케이션 로그에서 기동 완료 확인
2 헬스체크 통과 /health 또는 지정 엔드포인트 200 응답
3 프록시 트래픽 전환 로드밸런서 또는 nginx upstream 변경
4 이전 버전 종료 graceful shutdown, 처리 중 요청 완료 후

헬스체크 엔드포인트는 반드시 있어야 한다. 단순 HTTP 200 반환도 되는데, DB 커넥션 정상 여부나 주요 의존 서비스 응답 여부까지 포함하면 더 정확하다. 기동만 됐고 실제 요청은 처리 못 하는 상태를 헬스체크가 걸러줘야 의미가 있다.

롤백 준비도 배포 전에 확인해두는 편이다. 이전 버전 JAR나 Docker 이미지가 살아있는지, 롤백 명령을 실제로 실행해봤는지. 장애 상황에서 처음 해보는 작업은 항상 실수가 있다. 미리 한 번씩 흘려보는 게 낫다.

이번처럼 작은 수정이더라도 배포는 동일한 프로세스로 처리한다. "작은 변경이니까 빠르게 올리면 되지"라는 생각이 배포 사고의 시작이 되는 경우를 여러 번 봤기 때문에, 규모와 상관없이 순서는 동일하게 가져간다.

작업 규모는 크지 않았지만, 이런 수정들이 쌓이면서 시스템 전체의 신뢰도가 조금씩 올라간다. 개선 포인트를 발견하고, 안전하게 반영하고, 배포까지 정상적으로 마무리하는 사이클이 반복될수록 전체 플랫폼이 조금 더 단단해지는 느낌이 있다.

댓글 0

첫 댓글 달아줘.