Spring Boot 3.3에서 4.0으로 올리다가 이틀을 날린 이야기
사이드 프로젝트의 Spring Boot 버전을 올리며 Jackson 3 패키지 변경, properties 키 소멸, 서드파티 호환 문제로 삽질한 과정을 기록합니다.

사이드 프로젝트로 간단한 중고 거래 API를 혼자 만들어 운영하고 있었습니다. Spring Boot 3.3 기반이고, 상품 등록과 채팅 알림 정도가 전부인 작은 서비스입니다. 사용자도 지인 몇 명뿐이라 부담이 없었고, 그래서 "어차피 작은 프로젝트니까 4.0 바로 올려보자"라고 가볍게 시작했습니다.
월요일 저녁이었습니다. build.gradle에서 3.3.x를 4.0.0으로 바꾸고 빌드를 돌렸습니다. 터미널에 빨간 글씨가 올라오기 시작한 건 3초 만이었습니다.
첫날 저녁: 빌드부터 안 된다
처음 마주한 에러는 Jackson 관련이었습니다. Spring Boot 4.0이 Jackson 3.x를 기본으로 들고 오면서, 패키지 경로가 통째로 바뀐 겁니다. com.fasterxml.jackson이 tools.jackson이 됐습니다.
// 컴파일 에러가 난 import들
import com.fasterxml.jackson.annotation.JsonProperty; // 존재하지 않음
import com.fasterxml.jackson.annotation.JsonIgnore; // 존재하지 않음DTO 클래스가 열두 개쯤 있었는데, 전부 빨간 줄이 떴습니다. IDE에서 전체 검색-치환으로 com.fasterxml.jackson을 tools.jackson으로 일괄 변경했습니다. 여기까지는 10분이면 끝나는 단순 작업이었습니다.
문제는 그 다음이었습니다. 직접 만든 코드는 고치면 되는데, 서드파티 라이브러리가 내부적으로 Jackson 2를 쓰고 있으면 이야기가 달라집니다. 채팅 알림에 쓰고 있던 Firebase Admin SDK가 정확히 이 경우였습니다. Jackson 2와 3이 동시에 클래스패스에 올라가면서 NoSuchMethodError가 터졌습니다.
java.lang.NoSuchMethodError: 'com.fasterxml.jackson.core.JsonGenerator
tools.jackson.databind.ser.std.StdSerializer.createGenerator(...)'에러 메시지를 보고 한숨이 나왔습니다. 이건 제가 고칠 수 있는 문제가 아니었습니다. Firebase Admin SDK 쪽에서 Jackson 3을 지원하는 버전을 내놓아야 하는 건데, GitHub Issues를 뒤져보니 아직 대응 중이라는 코멘트만 달려 있었습니다.
결국 Firebase 알림 기능을 임시로 비활성화하고 넘어가기로 했습니다. 사용자가 지인 몇 명이라 카카오톡으로 알려주면 되니까요. 깔끔한 해결은 아니었지만, 여기서 막혀 있으면 다른 작업을 아예 진행할 수 없었습니다.
첫날 밤: 빌드는 됐는데 테스트가 깨진다
자정쯤 빌드가 드디어 성공했습니다. 기쁜 마음으로 ./gradlew test를 돌렸는데, 20개 테스트 중 7개가 실패했습니다.
가장 많이 깨진 건 API 응답 검증 테스트였습니다. 상품 등록 API의 응답에서 createdAt 필드가 이상하게 바뀌어 있었습니다.
// 기대값 (Jackson 2)
{ "createdAt": "2026-04-24T21:30:00" }
// 실제 응답 (Jackson 3)
{ "createdAt": [2026, 4, 24, 21, 30, 0] }LocalDateTime이 ISO 문자열이 아니라 숫자 배열로 직렬화된 겁니다. Jackson 3에서 JavaTimeModule의 기본 동작이 바뀌었고, Spring Boot의 자동 구성이 이걸 완전히 잡아주지 못하고 있었습니다.
ObjectMapper 설정을 명시적으로 잡아서 해결했습니다.
@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> builder
.modules(new JavaTimeModule())
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
}이걸 추가하고 나니 날짜 관련 테스트는 통과했습니다. 하지만 남은 실패 2건이 더 골치였습니다. 상품 상세 조회 API에서 @JsonIgnoreProperties(ignoreUnknown = true)를 붙인 DTO가 있었는데, Jackson 3에서 이 어노테이션의 동작이 미묘하게 달라져서 이전에는 무시되던 필드가 역직렬화 에러를 내고 있었습니다. 이건 릴리스 노트 어디에도 명확하게 안 나와 있어서 찾는 데만 40분이 걸렸습니다.
새벽 2시에 테스트 20개 전부 초록색을 확인하고 잠들었습니다.
둘째 날 오후: 조용한 장애
다음 날 점심 먹고 로컬에서 서버를 띄워 이것저것 눌러봤습니다. 상품 등록은 되고, 조회도 되고, 검색도 됩니다. "어, 생각보다 괜찮은데?"라고 생각하던 순간, 최근 본 상품 기능이 동작하지 않는 걸 발견했습니다.
이 기능은 Redis에 최근 조회 상품 ID를 저장하는 건데, 에러도 안 나고 로그도 깨끗한데 Redis에 데이터가 안 쌓이고 있었습니다.
원인은 application.yml이었습니다.
# 기존 설정 — Spring Boot 3.x에서는 deprecated 경고만 냈음
spring:
redis:
host: localhost
port: 6379Spring Boot 4.0에서는 spring.redis.*를 아예 인식하지 않습니다. spring.data.redis.*로 바꿔야 했습니다.
# 수정 후
spring:
data:
redis:
host: localhost
port: 6379이 두 줄 차이 때문에 Redis 연결이 조용히 실패하고 있었습니다. 컴파일 에러도 없고, 런타임 예외도 안 나고, 그냥 캐시가 동작하지 않는 것뿐이었습니다. 테스트에서 이걸 못 잡은 이유는, 테스트 환경에서는 embedded Redis를 쓰면서 별도 설정 파일을 사용하고 있었기 때문입니다.
이게 마이그레이션에서 제일 무서운 유형이라고 느꼈습니다. 빌드도 되고 테스트도 통과하는데, 실제 환경에서만 기능이 빠져 있는 것.
properties-migrator를 돌렸는데
그 뒤에 Spring Boot 팀이 제공하는 spring-boot-properties-migrator를 뒤늦게 알고 추가했습니다.
runtimeOnly 'org.springframework.boot:spring-boot-properties-migrator'서버를 다시 띄우니 로그에 변경된 설정 키 목록이 출력됐습니다. Redis 외에도 server.shutdown 기본값 변경, Virtual Threads 기본 활성화 같은 항목이 나왔습니다. "이걸 처음부터 넣었으면 한 시간은 아꼈을 텐데"라는 생각이 들었습니다.
그런데 이것도 만능은 아니었습니다. migrator가 잡아주는 건 Spring Boot 코어에서 관리하는 키뿐이고, 서드파티 starter가 자체적으로 바꾼 키는 알려주지 않습니다. 실제로 로깅 관련 설정 하나를 migrator가 놓쳤고, 그건 직접 공식 문서를 대조하면서 찾아야 했습니다.
돌아보면
화요일 저녁에 겨우 모든 기능이 정상 동작하는 걸 확인했습니다. "버전 번호 하나 올리는 일"이라고 생각한 작업에 이틀이 걸렸습니다.
시간을 가장 많이 잡아먹은 건 세 가지였습니다.
첫째, 서드파티 라이브러리의 Jackson 3 미지원. 내 코드는 금방 고치는데, 남의 라이브러리가 아직 대응하지 않으면 우회하거나 기다리는 수밖에 없습니다. 마이그레이션 전에 의존성 목록을 뽑아서 Jackson 3 호환 여부를 먼저 확인했어야 했습니다.
둘째, 소리 없이 죽는 설정 키. deprecated 경고를 내주던 시절에 고쳐뒀으면 마이그레이션 날 고생하지 않았을 겁니다. 경고 로그를 습관적으로 무시한 대가를 톡톡히 치렀습니다.
셋째, 테스트 환경과 실제 환경의 차이. 테스트에서 embedded Redis를 쓰는 건 편하지만, 설정 경로 같은 환경 차이를 가려버리는 부작용이 있었습니다. 이번 일을 계기로 통합 테스트 설정 파일에도 운영 환경과 같은 키 구조를 쓰도록 바꿨습니다.
사실 이 정도 규모의 프로젝트에서 이틀이면 양호한 편일 수도 있습니다. JPA N+1 문제를 잡을 때도 원인을 찾기까지 비슷한 시간이 걸렸으니까요. 다만 그때는 "로그에 단서가 있다"는 방향이라도 있었는데, 이번 properties 문제는 로그에 아무것도 안 남아서 더 답답했습니다.
다음에 메이저 버전 업그레이드를 할 때는 migrator부터 넣고, 의존성 호환 목록부터 확인하고, 배포 전 체크리스트에 "deprecated 경고 제로 확인" 항목을 추가할 생각입니다. 이번에 배운 건 거창한 교훈이 아니라 그냥 순서의 문제였습니다. 순서만 바꿨으면 이틀이 반나절이었을 겁니다.