Spring Boot 3.x → 4.0 마이그레이션: 실제로 깨지는 것들 (2026년 9월)

Spring Boot 4.0 마이그레이션 자료를 정리하다가 이상한 걸 발견했습니다. 여러 글이 “Java 21 이상 필수”라고 적어놨는데, 공식 시스템 요구사항 문서를 열어보니 최소 17이더군요.

이런 게 몇 군데 더 있었습니다. 그래서 릴리스 노트를 요약하는 대신 공식 마이그레이션 위키를 처음부터 읽고 정리했습니다.

실제로 올릴 때 가장 먼저 부딪히는 건 새 기능이 아니라 이름이 바뀐 스타터입니다. spring-boot-starter-webspring-boot-starter-webmvc가 됐고 OAuth2 스타터 셋에는 전부 security- 접두어가 붙었습니다. 여기서 빌드가 멈춥니다.

먼저 — Java 21이 필수라는 말은 사실이 아닙니다

공식 시스템 요구사항 문서에 적힌 내용은 이렇습니다.

항목 요구 버전
Java 17 이상
Spring Framework 7.0.9 이상
Jakarta EE 11
Servlet 6.1 (Tomcat 11.0.x, Jetty 12.1.x)
Maven 3.6.3 이상
Gradle 8.14 이상 또는 9.x
GraalVM Community 25

혼동이 생긴 이유는 짐작이 갑니다. Spring 팀이 Java 25 LTS를 권장 타깃으로 안내하고 있고, 가상 스레드를 제대로 쓰려면 21 이상이 필요하니까요. 그래도 권장과 최소는 다릅니다. Java 17에 머물러 있는 팀도 4.0으로 올라갈 수 있습니다.

또 하나. 4.0은 이미 성숙 단계입니다. 2025년 11월에 4.0.0이 나왔고 2026년 9월 현재 4.0.8까지 패치됐으며, 4.1.1도 출시돼 있습니다. .0 릴리스를 밟는 위험은 이제 없습니다.

깨지는 방식은 세 가지다

4.0의 변경 항목은 100개가 넘습니다. 전부 외울 필요는 없고, 어떻게 깨지는지로 나눠 보면 대응 순서가 정해집니다.

첫째는 빌드가 그 자리에서 멈추는 것입니다. 못 보고 지나칠 수가 없으니 위험하지 않습니다. 둘째는 빌드는 통과하고 기동하거나 첫 호출에서 터지는 것으로, 테스트가 있으면 대개 잡힙니다. 셋째가 문제입니다. 빌드도 되고 기동도 되는데 동작만 조용히 달라지는 항목들이 있습니다. 운영 중에 언젠가 드러나고, 그때까지 아무도 모릅니다.

정리하면서 느낀 건 ③에 시간을 써야 한다는 겁니다. ①은 어차피 컴파일러가 알려주니까요.

① 빌드가 멈추는 것 — 스타터 이름 변경

4.0은 모듈 구조를 전면 재편했습니다. 규칙은 spring-boot-<기술> / spring-boot-starter-<기술>이고, 이 과정에서 이름이 바뀐 스타터가 있습니다.

3.x 4.0
spring-boot-starter-web spring-boot-starter-webmvc
spring-boot-starter-web-services spring-boot-starter-webservices
spring-boot-starter-aop spring-boot-starter-aspectj
spring-boot-starter-oauth2-client spring-boot-starter-security-oauth2-client
spring-boot-starter-oauth2-resource-server spring-boot-starter-security-oauth2-resource-server
spring-boot-starter-oauth2-authorization-server spring-boot-starter-security-oauth2-authorization-server

전용 스타터가 없어서 그냥 쓰이던 것들도 이제 스타터가 필요합니다. Flyway와 Liquibase가 대표적입니다.

<!-- 3.x: 라이브러리만 넣으면 자동 구성됐다 -->
<dependency>
  <groupId>org.flywaydb</groupId>
  <artifactId>flyway-core</artifactId>
</dependency>

<!-- 4.0: 스타터를 명시해야 자동 구성된다 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-flyway</artifactId>
</dependency>

테스트 의존성도 같은 원리로 쪼개졌습니다. 스타터마다 spring-boot-starter-<기술>-test 짝이 생겼습니다. 예를 들어 @WithMockUser, @WithUserDetails를 쓰려면 이제 spring-boot-starter-security-test가 필요합니다.

WAR 배포를 한다면

spring-boot-starter-tomcatspring-boot-starter-tomcat-runtime 으로 바꿔야 합니다. 그리고 server.forward-headers-strategy가 WAR 배포에는 더 이상 적용되지 않으므로, 프록시 뒤에 있다면 ForwardedHeaderFilter 빈을 직접 등록해야 합니다.

② 런타임에 터지는 것

Jackson 2 → 3

가장 광범위한 변경입니다. 그룹 ID부터 바뀌었습니다.

com.fasterxml.jackson  →  tools.jackson

예외가 하나 있습니다. jackson-annotationscom.fasterxml.jackson.core 그룹에 그대로 남아 있습니다. 전부 치환하면 오히려 깨집니다.

클래스와 애너테이션도 이름이 바뀌었습니다.

3.x 4.0
Jackson2ObjectMapperBuilderCustomizer JsonMapperBuilderCustomizer
JsonObjectSerializer ObjectValueSerializer
JsonValueDeserializer ObjectValueDeserializer
@JsonComponent @JacksonComponent
@JsonMixin @JacksonMixin

프로퍼티도 한 단계 깊어졌습니다.

# 3.x
spring:
  jackson:
    read: { ... }
    write: { ... }
    parser: { ... }

# 4.0
spring:
  jackson:
    json:
      read: { ... }
      write: { ... }

그리고 ObjectMapper 빈을 정의해 직렬화를 커스터마이징하던 코드는 더 이상 의도대로 동작하지 않습니다. 4.0은 포맷별 매퍼를 자동 구성합니다 — JSON은 JsonMapper, XML은 XmlMapper. 재정의하려면 ObjectMapper가 아니라 이 타입으로 빈을 등록해야 합니다.

당장 다 못 옮기겠다면 탈출구가 있습니다. spring-boot-jackson2 모듈(이미 deprecated)을 쓰거나, 다음 프로퍼티로 Jackson 2 기본값을 유지할 수 있습니다.

spring.jackson.use-jackson2-defaults=true

임시방편입니다. 언젠가는 옮겨야 합니다.

테스트 자동 구성이 분리됐다

@SpringBootTest가 알아서 챙겨주던 것들이 빠졌습니다.

MockMvc 를 쓰던 테스트에는 @AutoConfigureMockMvc 를 붙여야 합니다. TestRestTemplate@AutoConfigureTestRestTemplate 과 test 스코프의 spring-boot-resttestclient 의존성이 함께 필요하고, WebTestClient@AutoConfigureRestTestClientspring-boot-restclient 를 요구합니다.

TestRestTemplate은 패키지도 바뀌었습니다: org.springframework.boot.resttestclient.TestRestTemplate.

@MockBean@SpyBean은 제거됐습니다. @MockitoBean, @MockitoSpyBean으로 바꿔야 하는데, 단순 치환으로 끝나지 않는 차이가 있습니다. 새 애너테이션은 테스트 클래스의 필드에만 쓸 수 있고 @Configuration 클래스 안에서는 쓸 수 없습니다. 공용 테스트 설정 클래스에 @MockBean을 모아두던 구조라면 설계를 바꿔야 합니다.

3.4에서 deprecated였던 MockitoTestExecutionListener도 제거됐습니다. Mockito가 제공하는 MockitoExtension을 직접 쓰면 됩니다.

클래스가 이동했다

spring.factories나 직접 임포트에 걸립니다.

클래스 이동
EnvironmentPostProcessor org.springframework.boot.envorg.springframework.boot
BootstrapRegistry 계열 org.springframework.bootorg.springframework.boot.bootstrap
@EntityScan org.springframework.boot.persistence.autoconfigure.EntityScan
@PropertyMapping org.springframework.boot.test.context

③ 조용히 동작만 바뀌는 것 — 여기를 보세요

빌드도 통과하고 기동도 되는데 결과가 달라지는 항목들입니다. 테스트가 없으면 운영에서 발견합니다.

Spring Batch가 DB에 메타데이터를 쓰지 않는다

4.0의 Spring Batch는 기본이 인메모리입니다. 업그레이드하면 BATCH_JOB_INSTANCE 같은 메타데이터 테이블에 더 이상 기록하지 않습니다.

재실행 방지, 중단 지점 재개, 실행 이력 조회가 전부 이 메타데이터에 의존합니다. 조용히 사라지면 배치가 매번 처음부터 도는데 아무도 모릅니다.

<!-- DB 기반 메타데이터를 유지하려면 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-batch-jdbc</artifactId>
</dependency>

PropertyMapper가 null을 건너뛴다

PropertyMapper는 소스가 null이면 adapter와 predicate 메서드를 아예 호출하지 않도록 바뀌었습니다. null일 때도 매핑이 일어나길 기대한 코드는 조용히 동작이 달라집니다.

// 4.0: null 도 넘기고 싶다면 always() 를 명시
map.from(source::getValue).always().to(target::setValue);

기존 alwaysApplyingWhenNonNull()은 제거됐습니다.

기타 기본값 변경

  • Actuator 프로브가 기본 활성화 — liveness/readiness가 켜진 채로 뜹니다. 쿠버네티스 설정과 충돌하면 management.endpoint.health.probes.enabled로 끄세요.
  • DevTools LiveReload가 기본 비활성화 — spring.devtools.livereload.enabled=true로 되살립니다.
  • optional 의존성이 uber jar에서 빠집니다 — 필요하면 <includeOptional>true</includeOptional>.
  • 정적 리소스에 /fonts/ 포함** — PathRequest#toStaticResources가 폰트 경로를 포함합니다. 인증을 걸어야 한다면 .excluding(StaticResourceLocation.FONTS).

프로퍼티 이름 변경

3.x 4.0
spring.dao.exceptiontranslation.enabled spring.persistence.exceptiontranslation.enabled
spring.session.redis.* spring.session.data.redis.*
spring.session.mongodb.* spring.session.data.mongodb.*
spring.data.mongodb.* (비 Spring Data 항목) spring.mongodb.*
spring.kafka.retry.topic.backoff.random spring.kafka.retry.topic.backoff.jitter

이건 손으로 찾지 마세요. 도구가 있습니다.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-properties-migrator</artifactId>
  <scope>runtime</scope>
</dependency>

기동 시 바뀐 프로퍼티를 로그로 알려주고 런타임에 임시 변환까지 해줍니다. 마이그레이션이 끝나면 반드시 제거하세요. 계속 두면 잘못된 설정이 계속 동작하면서 문제를 가립니다.

아예 사라진 것

  • Undertow — 스타터와 임베디드 서버 지원 모두 제거. Servlet 6.1 비호환이 이유입니다. Tomcat이나 Jetty로 옮겨야 합니다.
  • 실행 가능 jar 스크립트 — 임베디드 launch script로 만들던 “fully executable” jar 지원이 빠졌습니다. init.d 서비스로 띄우던 배포 스크립트는 재작성이 필요합니다.
  • Spock 통합 — Groovy 5 비호환.
  • Spring Session의 Hazelcast·MongoDB 지원 — 각 팀으로 이관.
  • Pulsar Reactive 자동 구성
  • CLASSIC uber-jar 로더 — <loaderImplementation>CLASSIC</loaderImplementation> 설정을 지우세요.

안전한 순서

공식 가이드가 권하는 경로입니다. 한 번에 4.0으로 점프하지 마세요.

  1. 먼저 3.5.x 최신으로 올린다. 여기서 deprecation 경고를 전부 없앱니다. 4.0에서 제거된 것들은 대부분 3.x에서 미리 deprecated 됐습니다.
  2. spring-boot-properties-migrator를 넣고 기동한다. 프로퍼티 변경 목록을 로그로 받습니다.
  3. 대형 애플리케이션이라면 spring-boot-starter-classic을 경유한다. 3.x와 유사한 묶음을 제공하는 중간 단계 스타터입니다. 테스트는 spring-boot-starter-test-classic. 일단 이걸로 컴파일과 기동을 통과시킨 뒤, 실제로 쓰는 기술만 골라 모듈 스타터로 쪼개면 됩니다.
  4. ③ 유형을 하나씩 확인한다. 배치 메타데이터, PropertyMapper, Actuator 프로브.
  5. classic 스타터와 properties-migrator를 제거한다.

3번을 건너뛰면 의존성 오류 수백 개를 한꺼번에 마주하게 됩니다. 이 단계가 있다는 걸 모르는 경우가 많은데, 규모가 있는 프로젝트라면 여기서 시간이 갈립니다.

이 블로그의 글 중 손봐야 할 것

이 글을 쓰면서 사이트에 발행된 개발 글 39편의 코드를 4.0 기준으로 훑어봤습니다. 네 편에서 수정이 필요한 부분이 나왔습니다.

문제 조치
Spring Security OAuth2 Client 완벽 가이드 spring-boot-starter-oauth2-client 의존성 security- 접두어 추가
Spring Batch DuplicateKeyException 해결 BATCH_JOB_INSTANCE 전제가 4.0에서 깨짐 spring-boot-starter-batch-jdbc 전제 명시
Spring RestDocs vs Swagger 비교 MockMvc가 자동 제공된다는 전제 @AutoConfigureMockMvc 필요
Spring Security 필터 체인 완벽 해부 javax.servlet 표기 jakarta.servlet

Spring Security 관련 글은 4.0이 Spring Security 7을 끌고 온다는 점도 함께 봐야 합니다. 인증·인가 쪽은 OAuth 2.1 변경점 정리와 묶어서 점검하는 편이 낫습니다.

점검 목록

  • ☐ Java 17 이상, Maven 3.6.3+ / Gradle 8.14+ 인가
  • ☐ 3.5.x 최신으로 먼저 올려 deprecation 경고를 없앴는가
  • spring-boot-starter-webwebmvc 등 스타터 이름을 바꿨는가
  • ☐ Flyway·Liquibase 등 전용 스타터를 추가했는가
  • ☐ 테스트 의존성을 -test 짝으로 교체했는가
  • @MockBean/@SpyBean@MockitoBean/@MockitoSpyBean (필드에서만 사용)
  • ☐ MockMvc·TestRestTemplate에 @AutoConfigure...를 붙였는가
  • com.fasterxml.jacksontools.jackson (단, jackson-annotations는 제외)
  • ObjectMapper 커스터마이징을 JsonMapper 기준으로 바꿨는가
  • ☐ Spring Batch를 쓴다면 spring-boot-starter-batch-jdbc를 넣었는가
  • PropertyMapper에 null 의존 로직이 있는가
  • ☐ Actuator 프로브 기본 활성화가 배포 환경과 충돌하지 않는가
  • spring-boot-properties-migrator로 프로퍼티를 훑고 제거했는가
  • ☐ Undertow를 쓰고 있지 않은가

자주 묻는 질문

Q. Java 17에 머물러도 Spring Boot 4를 쓸 수 있나요? A. 됩니다. 최소 요구 버전이 17입니다. 다만 가상 스레드와 Scoped Values 같은 기능은 Java 21 이상이 필요하고, Spring 팀은 Java 25 LTS를 권장 타깃으로 안내합니다.

Q. 3.x에서 4.0으로 한 번에 올려도 되나요? A. 작은 프로젝트면 가능하지만 권장되지 않습니다. 3.5.x를 거치면 4.0에서 제거된 API가 deprecation 경고로 먼저 드러납니다. 경고를 다 없앤 뒤 올리는 것이 훨씬 빠릅니다.

Q. spring-boot-starter-classic은 계속 써도 되나요? A. 마이그레이션 중간 단계용입니다. 컴파일을 통과시키는 데 쓰고, 실제로 사용하는 기술의 모듈 스타터로 쪼갠 뒤 제거하세요. 계속 두면 모듈화의 이점(불필요한 의존성 제거, 기동 시간 단축)을 얻지 못합니다.


이 글은 Spring Boot 4.0 공식 마이그레이션 가이드시스템 요구사항 문서를 2026년 9월 기준으로 정리한 것입니다. 실제 적용 결과는 사용하는 기술 조합에 따라 달라질 수 있으니, 운영 반영 전 스테이징에서 검증하시기 바랍니다.

참고 자료

함께 읽으면 좋은 글

“Spring Boot 3.x → 4.0 마이그레이션: 실제로 깨지는 것들 (2026년 9월)”에 대한 3개의 생각

댓글 남기기