Showing Posts From
버전
- 01 Dec, 2025
Spring Boot 버전 업그레이드: 일주일을 잃다
Spring Boot 버전 업그레이드: 일주일을 잃다 아, 그거요. 이 이야기를 하려면 먼저 지난 월요일 아침으로 돌아가야 한다. 커피를 마시면서 슬랙을 보다가 기술 뉴스 채널에서 "Spring Boot 3.2.0 릴리즈! 획기적인 성능 개선과 새로운 기능들"이라는 메시지를 봤다. 그 순간의 나는 정말 한심한 수준의 낙관주의자였다. 마치 신작 게임이 나왔다고 생각하고 선구매하는 게이머처럼, 나는 "오, 이 정도면 업그레이드 간단하겠는데?"라고 생각했다. 지금 생각해보니 그게 모든 고통의 시작이었다. 호기심 같은 업그레이드의 시작 현재 프로젝트는 Spring Boot 2.7.15 버전을 쓰고 있었다. 안정적이고, 버그도 별로 없고, 팀원들도 익숙한 버전. 근데 왜 하필 그날 업그레이드를 시작했을까? 이유는 정말 초라한 수준이었다. "요새 Spring Boot 3.2를 안 쓰면 구시대 개발자처럼 보일 거 같은데?" 이 정도의 이유였다.build.gradle 파일을 열고 springBootVersion을 3.2.0으로 바꿨다. 엔터를 쳤다. IDE는 즉시 빨간 줄을 그었다. 대량의 빨간 줄이었다. 처음엔 괜찮을 거라고 생각했다. 점진적으로 해결하면 되겠지, 하는 마음으로. 결국 모든 프로젝트의 죽음은 이런 순진함에서 시작된다. 의존성 지옥의 나락으로 첫 번째 문제는 Gradle 버전이었다. Spring Boot 3.2.0은 Gradle 7.6 이상을 요구했는데, 우리 프로젝트는 7.3을 쓰고 있었다. 간단하다고 생각했다. Gradle 버전을 올리면 되겠지. 그런데 Gradle 버전을 올리니까 이번엔 Java 버전 문제가 나왔다. 프로젝트에서 Java 11을 쓰고 있었는데, Spring Boot 3.x는 최소 Java 17을 요구한다. 알겠다, Java 버전도 올리자. Java 17로 올렸다. 이제 정말 되겠지? 아니었다. 이건 마치 도미노 같았다. 한 개를 넘치니까 다 넘어지는 그런 느낌이었다. 프로젝트에서 쓰던 Querydsl이 호환성 문제를 일으켰다. Maven 중앙 저장소를 뒤져보니, 우리가 쓰던 Querydsl 4.4.0은 Spring Boot 3.x와 호환성 문제가 있었다. 최신 버전인 5.0.0으로 올려야 했다. Querydsl 5.0.0으로 올렸다. 그럼 이제 되겠지? gradle build를 실행했다. 또 다른 에러가 나왔다. javax 패키지들이 사라졌다는 것이었다. Spring Boot 3.0부터 javax.*에서 jakarta.*로 완전히 전환되었기 때문이다. 이건 단순 버전 업 문제가 아니라 전체 구조 변경이었다. // 이게 모두 바뀌어야 함 import javax.persistence.Entity; import javax.persistence.GeneratedValue;// 이렇게 import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue;프로젝트 전체에서 javax를 jakarta로 바꾸는 작업이 시작되었다. IDE의 대량 검색-바꾸기 기능을 썼지만, 손으로 일일이 확인해야 하는 경우도 많았다. 왜냐하면 javax.servlet도 바꿔야 하고, javax.validation도 바꿔야 하고, javax.mail도... 이 작업만 3시간이 걸렸다. 화요일: 첫 번째 깨달음 화요일 오전. 슬랙에서 후배 개발자가 물어봤다. "선임님, 이 PR 리뷰 좀 해주실 수 있나요?" 나는 "오케이, 잠깐만" 이라고 했지만, 내 손은 여전히 EntityManagerFactory 설정 파일을 수정 중이었다. 일주일에 보기로 했던 신입 면접 준비도 날려먹었다. 점심 시간에 회사 근처 김치찌개집에서 사장님과 뵈면서도 "요새 너무 바빠서 ㅋㅋㅋ" 하고 웃었지만, 사실 웃음 뒤에는 절망이 묻어있었다."Spring Boot 3.2 하면서 뭐 이렇게 복잡해?" 라고 생각했지만, 알고 보니 그건 시작일 뿐이었다. 수요일: Jackson의 배신 수요일이 되자, 빌드 에러는 줄었지만 이번엔 런타임 에러가 터지기 시작했다. API 응답이 JSON으로 변환되지 않았다. 원인은 Jackson 라이브러리 업데이트였다. 예전 버전에서는 기본적으로 작동하던 기능들이 새 버전에서는 명시적으로 설정해야 했다. // 기존엔 이게 자동으로 됐는데... LocalDateTime.now()// 이제는 이렇게 설정해야 함 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createdAt;프로젝트 곳곳에 있는 API 응답 DTO 수십 개를 일일이 확인하고 수정했다. 각각의 LocalDateTime에 어노테이션을 달아야 했다. 이 작업을 하면서 나는 진짜로 "차라리 Spring Boot 2.7 유지하는 게 낫지 않을까?"라는 생각을 처음으로 진지하게 해봤다. 슬랙에 투덜거렸다. "이거 왜 이리 복잡하냐고... 대충 호환 모드 같은 게 없나?" 팀장이 답장했다. "그냥 고민 좀 해봐. 급할 건 없으니까." 하지만 그건 팀장 말이었고, 내 심리 상태는 이미 "이건 내가 해결해야 하는 문제"라는 강박에 빠져있었다. 목요일: 데이터베이스 커넥션의 악몽 목요일. 애플리케이션이 어느 정도 떠올랐다. 그런데 데이터베이스 커넥션에서 문제가 터졌다. Connection pool 에러였다. Spring Boot 3.x에서는 기본 데이터베이스 커넥션 풀이 HikariCP에서 다른 것으로 변경되었을 뿐 아니라, 설정 방식도 살짝 달라져있었다. # 예전엔 이게 먹혔는데 spring: datasource: hikari: maximum-pool-size: 20# 새 버전에선 이렇게 해야 함 (세부사항이 좀 다름) spring: datasource: hikari: max-lifetime: 1800000 idle-timeout: 900000설정 파일을 뒤지고, Spring Boot 공식 문서를 뒤지고, 스택오버플로우를 샅샅이 뒤졌다. "아 그거요" 하면서 몇 가지 설정을 건드렸더니, 로컬 환경에서는 돌아갔다. 근데 스테이징 환경에서는 안 돌아갔다. 왜냐하면 스테이징 환경은 우리 프로젝트가 쓰는 여러 마이크로서비스 중 하나일 뿐이었고, 다른 서비스들도 같은 데이터베이스를 쓰고 있었기 때문이다. 커넥션 풀 설정을 건드리니까 다른 서비스들의 연결이 끊어졌다. 데이터베이스 팀에 연락했다. 죄송합니다, 제가 실수했습니다. 다시 설정할 테니 잠깐만요. 금요일: 마이크로미터 메트릭 문제 금요일이 되자, 모니터링 시스템이 문제를 일으켰다. Prometheus 메트릭 수집이 안 되고 있었다. Spring Boot 3.x에서 메트릭 수집 방식이 바뀌면서 기존 설정이 더 이상 작동하지 않았다. Micrometer 라이브러리의 주요 메서드와 클래스 이름이 바뀌었다. // 예전 코드 MeterRegistry meterRegistry; meterRegistry.counter("custom.metric").increment();// 새 코드 (내부 동작 방식이 좀 다름) @Timed(value = "custom.metric") public void someMethod() { // ... }금요일 오후 3시. 나는 모니터링 알림이 작동하는 것을 보기 위해 필사적이었다. 왜냐하면 월요일에 본배포가 예정되어있었기 때문이다. 퇴근 시간을 넘겨서 계속 작업했다. 6시, 7시, 8시... 9시가 되어서야 메트릭 수집이 정상화되었다. 집에 도착한 건 10시가 넘었다. 아내는 이미 잠들어있었다. 나는 라면을 끓여먹고 침대에 쓰러졌다. 둘째 주 월요일: 통합 테스트의 지옥 둘째 주 월요일. 로컬 빌드는 성공했지만, CI/CD 파이프라인에서 테스트가 실패했다. "어? 로컬에선 되는데?" 이건 개발자가 가장 싫어하는 상황이다. 로컬 환경과 테스트 환경의 차이 때문이었다. 통합 테스트에서 쓰는 TestContainers의 버전과 Spring Boot 3.x의 호환성 문제였다. Docker를 통해 테스트 DB를 구성하는데, 이 과정에서 버전 미스매치로 인한 타이밍 이슈가 발생했다. 테스트를 하나씩 실행하면 성공하는데, 전체 테스트를 실행하면 일부가 실패했다. 이건 경합 조건(race condition) 같은 거였다. TestContainers 버전을 올렸다. 그럼 이제 되겠지? 아니었다. 이번엔 다른 테스트가 실패했다. 결국 나는 8시간을 테스트 디버깅에만 썼다. 마지막에는 테스트 타임아웃 설정을 조정하고, 데이터베이스 초기화 순서를 바꿔야 했다.셋째 주 초: 배포와 롤백의 악몽 업그레이드를 시작한 지 8일째. 드디어 본배포 준비가 됐다고 생각했다. 배포는 성공했다. 서버가 떠올랐다. 모든 것이 정상으로 보였다. 그런데 2시간 뒤, 모니터링 대시보드에서 에러율이 갑자기 올라갔다. 원인은 Spring Security 설정이었다. Spring Boot 3.x에서 보안 설정 방식이 바뀌면서, 기존 설정이 예상하지 못한 동작을 하고 있었다. 특정 엔드포인트에 대한 권한 체크가 너무 엄격해져있었다. 30분 뒤 롤백 결정. 내가 업그레이드한 Spring Boot 3.2 버전은 다시 2.7로 돌아갔다. 슬랙 타이머가 종료되었다. 돌아온 월요일: 후회와 깨달음 원점으로 돌아왔다. 일주일을 완전히 낭비했다. 이 시간에 내가 할 수 있었던 것들을 생각해봤다.신입사원 온보딩 프로세스 개선 레거시 코드 리팩토링 테크 로드맵 수립 아내와 시간 보내기 충분한 숙면대신 나는 뭘 했나? javax → jakarta 변환하고, 테스트 타임아웃 튜닝하고, Spring Security 설정을 삽질했다. 후배들이 물어봤다. "선임님, Spring Boot 3.2는 괜찮던데요?" "음, 아직 좀 이른 거 같아. 마이너 버전 유지하자." 팀장은 나에게 물었다. "이번에 뭘 배웠어?" 나는 웃었다. "아, 그거요. 버전 업그레이드는 정말 신중해야 한다는 거요." 얻은 교훈 (그리고 다음번엔 이렇게 할 거) 일주일을 낭비한 대가로, 나는 몇 가지 중요한 걸 배웠다. 먼저, 주요 메이저 버전 업그레이드는 절대 혼자 끙끙대며 하지 말 것. 팀과 함께 계획을 세우고, 명확한 일정과 롤백 계획을 세워야 한다. 둘째, 마이너 버전 유지의 가치를 인정할 것. Spring Boot 2.7이 LTS 버전이고 충분히 안정적이라면, 굳이 3.x로 업그레이드할 이유가 있을까? 비용과 이득을 따져봐야 한다. 셋째, 호환성 문서를 미리 읽을 것. Spring 공식 마이그레이션 가이드가 있다. 읽어봤으면 훨씬 빨랐을 거다. 넷째, 의존성 버전 고정의 중요성. Gradle의 dependency lock을 활용하면, 버전 충돌을 미리 감지할 수 있다. 마지막으로, 신중함의 가치. 호기심과 선도적 기술 도입은 좋지만, 모든 비용을 개인이 떠안는 건 아니다. 에필로그: 그래도 계속 개발하는 이유 이 이야기를 읽으면, "개발 일이 답답하네. 그냥 그만두지?"라고 생각할 수도 있다. 근데 솔직히, 이런 일들이 반복되어도 나는 개발을 계속한다. 왜냐하면 이 불편함 뒤에 무언가가 있기 때문이다. 새로운 기술을 배우는 과정은 고통스럽지만, 배운 후에는 문제 해결 능력이 한 단계 올라간다. Spring Boot 3.x로 업그레이드하면서 나는 Jakarta 네임스페이스, 마이크로미터 메트릭, Spring Security 설정 등에 대해 깊이 있게 배웠다. 다음에 비슷한 문제가 나타나면, 훨씬 빨리 해결할 수 있을 거다. 그리고 이 경험이, 다른 후배들에게 "신중함이 얼마나 중요한지"를 가르쳐주는 좋은 사례가 될 수도 있다. 결국 개발이라는 일은 이런 반복이다. 실패하고, 배우고, 다시 시도하고. 매번 조금씩 성장한다. 물론 일주일을 날린 건 별로 기분 좋지 않은 결과지만, 그래도 내일은 또 다른 문제를 해결하러 깃을 켤 것이다. 왜냐하면 개발자는 그렇게 되어있으니까.차라리 처음부터 "유지보수 vs 혁신"을 제대로 따져볼 걸.