Spring Boot 4 신기능 총정리: API 버전 관리부터 Jackson 3 마이그레이션까지

이번 포스팅에서는 2025년 11월에 정식 출시된 Spring Boot 4와 Spring Framework 7의 신기능에 대해서 정리하고자 한다. 이번 메이저 버전은 스타터 모듈 구조가 통째로 바뀌었고, REST API 버전 관리가 프레임워크에 내장되었으며, Jackson이 3으로 올라가면서 패키지명부터 달라졌다. @Retryable이 Spring Framework 코어에 들어온 것도 실무에서 체감이 클 변화다. 각 변경사항을 코드와 설정 예시 중심으로 살펴본다.

REST API도 이제 버전 관리가 기본이다

Spring Boot 4 API Versioning, 프레임워크가 직접 해준다 참고

그동안 REST API 버전 관리는 URL에 /v1/을 직접 붙이거나, 프로젝트마다 커스텀 어노테이션을 만들어 쓰는 식이었다. Spring Framework 7부터는 @RequestMapping 계열 어노테이션에 version 속성이 생겼다. 프레임워크 자체에서 버전 라우팅을 처리해 주므로 별도 라이브러리가 필요 없다.

컨트롤러에서 버전 선언하기

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AccountController {

    // v1: 기본 계좌 정보만 반환
    @GetMapping(value = "/accounts/{id}", version = "1")
    public AccountV1 getAccountV1(@PathVariable Long id) {
        return accountService.findBasicById(id);
    }

    // v1.1: 거래 내역을 포함한 상세 정보 반환
    @GetMapping(value = "/accounts/{id}", version = "1.1")
    public AccountV2 getAccountV1_1(@PathVariable Long id) {
        return accountService.findDetailedById(id);
    }
}
Java

같은 URL /accounts/{id}version 값만 다르게 붙이면 된다. 클라이언트가 보내는 버전 정보에 따라 Spring이 알아서 핸들러를 매핑한다.

버전 전략 설정하기

버전 정보를 어디서 읽을지는 WebMvcConfigurerconfigureApiVersioning 메서드에서 설정한다. Spring Framework 7은 헤더, 경로 세그먼트, 쿼리 파라미터, 미디어 타입 총 4가지 전략을 기본 제공한다.

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class ApiVersionConfig implements WebMvcConfigurer {

    @Override
    public void configureApiVersioning(ApiVersionConfigurer configurer) {
        // 요청 헤더 "API-Version" 값으로 버전을 판별한다
        configurer.useRequestHeader("API-Version")
                .addSupportedVersions("1", "1.1", "2")
                .addDeprecatedVersions("1"); // v1은 deprecated 처리
    }
}
Java

useRequestHeader("API-Version")를 지정하면 클라이언트가 API-Version: 1.1 헤더를 보낼 때 해당 버전의 핸들러가 동작한다. addDeprecatedVersions("1")는 RFC 9745에 따라 응답 헤더에 deprecation 힌트를 자동 삽입한다. 등록되지 않은 버전으로 요청하면 400 에러가 나온다.

경로 기반 버전 관리가 필요하다면 다음과 같이 변경한다.

@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
    // URL의 첫 번째 경로 세그먼트를 버전으로 사용한다
    // 예: /1.1/accounts/42 또는 /v2/accounts/42
    configurer.usePathSegment(0)
            .addSupportedVersions("1", "1.1", "2");
}
Java

경로 기반 전략에서는 URL 패턴이 /{version}/accounts/{id} 형태가 된다. usePathSegment(0)이 URL의 첫 번째 세그먼트를 버전 값으로 파싱한다. 내부적으로 SemanticApiVersionParser가 메이저, 마이너, 패치 숫자를 Comparable로 변환하여 버전 비교에 사용한다.

RestClient에서 버전 지정하기

서버 간 통신에서도 RestClientWebClient가 API 버전 관리를 지원한다. ApiVersionInserter를 사용하면 요청 시 자동으로 버전 정보가 삽입된다.

import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.ApiVersionInserter;

@Configuration
public class RestClientConfig {

    @Bean
    public RestClient accountRestClient() {
        // 모든 요청에 API-Version 헤더를 자동으로 추가한다
        ApiVersionInserter versionInserter = ApiVersionInserter
                .useHeader("API-Version");

        return RestClient.builder()
                .baseUrl("https://api.example.com")
                .apiVersionInserter(versionInserter)
                .build();
    }
}
Java

ApiVersionInserter를 RestClient 빌더에 연결해 두면 이후 모든 요청에서 .apiVersion("1.1") 메서드 체이닝으로 버전을 지정할 수 있다. HTTP 인터페이스 클라이언트에서도 같은 방식으로 동작한다.

Jackson 3 전환, 패키지부터 달라졌다

Spring Boot 4는 Jackson 3을 기본 JSON 라이브러리로 채택했다. 가장 큰 변화는 패키지명이 com.fasterxml.jackson에서 tools.jackson으로 완전히 변경된 점이다. 이는 단순한 네이밍 변경이 아니라 Jackson 프로젝트의 거버넌스 이전에 따른 것이며, 기존 Jackson 2 코드와는 동일 클래스패스에서 공존이 가능하다.

주요 변경 사항 한눈에 보기

// Jackson 2 (Spring Boot 3.x)
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonProperty;

ObjectMapper mapper = new ObjectMapper();

// Jackson 3 (Spring Boot 4)
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.annotation.JsonProperty;

// ObjectMapper 대신 JsonMapper를 사용한다 (불변 객체)
JsonMapper mapper = JsonMapper.builder().build();
Java

Jackson 3에서 ObjectMapperJsonMapper으로 대체된다. JsonMapper은 불변(immutable) 객체로 설계되어 멀티스레드 환경에서 별도의 동기화 없이 안전하게 공유할 수 있다. 빌더 패턴을 통해 생성하며, 한 번 생성된 인스턴스의 설정은 변경할 수 없다.

Spring Boot 설정 프로퍼티 변경

# Spring Boot 3.x (Jackson 2)
spring:
  jackson:
    serialization:
      write-dates-as-timestamps: true
    deserialization:
      fail-on-unknown-properties: false

# Spring Boot 4 (Jackson 3)
spring:
  jackson:
    json:
      read:
        fail-on-unknown-properties: false
      write:
        write-dates-as-timestamps: true
YAML

설정 프로퍼티 경로가 spring.jackson.serialization.*에서 spring.jackson.json.write.*로, spring.jackson.deserialization.*에서 spring.jackson.json.read.*로 변경되었다. Jackson 3의 기본 동작도 달라졌는데, 프로퍼티가 알파벳 순으로 정렬되고(SORT_PROPERTIES_ALPHABETICALLY = true), 날짜는 타임스탬프 대신 ISO-8601 문자열로 직렬화된다.

어노테이션 이름 변경

// Spring Boot 3.x
@JsonComponent
public class CustomSerializer extends JsonObjectSerializer<MyEntity> {
    // ...
}

// Spring Boot 4
@JacksonComponent  // @JsonComponent → @JacksonComponent
public class CustomSerializer extends ObjectValueSerializer<MyEntity> {
    // JsonObjectSerializer → ObjectValueSerializer
}
Java

@JsonComponent@JacksonComponent로, JsonObjectSerializerObjectValueSerializer로 변경되었다. 커스텀 직렬화/역직렬화 로직을 구현한 클래스가 있다면 어노테이션과 부모 클래스명을 모두 변경해야 한다. Jackson 2와의 호환이 필요한 경우 spring-boot-jackson2 모듈을 추가하면 병행 사용이 가능하지만, 이 모듈은 향후 지원 중단 예정이다.

스타터가 잘게 쪼개졌다

스타터 구조도 많이 바뀌었다. spring-boot-autoconfigurespring-boot-test-autoconfigure JAR가 기술별로 분리되었고, 스타터 이름도 상당수 달라졌다. 애플리케이션에 불필요한 클래스가 포함되는 것을 줄이고, IDE 자동완성이 실제 사용하는 클래스만 보여주도록 하기 위한 변경이다.

스타터 이름 변경 매핑

<!-- Spring Boot 3.x pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- Spring Boot 4 pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <!-- web → webmvc 로 변경 -->
    <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <!-- aop → aspectj 로 변경 -->
    <artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>
XML

가장 많이 사용되는 spring-boot-starter-webspring-boot-starter-webmvc로 변경된 점이 실무에서 가장 큰 영향을 미친다. spring-boot-starter-web-servicesspring-boot-starter-webservices로, OAuth2 관련 스타터들은 spring-boot-starter-security-oauth2-* 형태로 security- 접두사가 추가되었다.

Gradle에서의 마이그레이션

// build.gradle (Spring Boot 4)
plugins {
    id 'org.springframework.boot' version '4.0.0'
    id 'io.spring.dependency-management' version '1.1.7'
    id 'java'
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test'
    // 테스트 스타터도 기술별로 분리되었다
}
Groovy

테스트 관련 자동 설정도 spring-boot-starter-<technology>-test 패턴으로 분리되었다. 예를 들어 웹 MVC 테스트에 필요한 자동 설정은 spring-boot-starter-webmvc-test에 담긴다. 이전 구조가 익숙한 개발자를 위해 spring-boot-starter-classic을 의존성에 추가하면 기존 방식으로도 동작하지만, 이 호환 스타터는 일시적 조치이므로 점진적 전환을 권장한다.

재시도와 동시성 제어, 이제 프레임워크가 해준다

Spring Retry를 별도로 추가해서 사용하던 @Retryable이 Spring Framework 7에 직접 통합되었다. 추가 의존성 없이 재시도 로직과 동시성 제한을 선언적으로 적용할 수 있다. 사용하려면 @EnableResilientMethods를 설정 클래스에 선언해야 한다.

@Retryable 내장 지원

import org.springframework.context.annotation.Configuration;
import org.springframework.resilience.annotation.EnableResilientMethods;

@Configuration
@EnableResilientMethods  // @Retryable, @ConcurrencyLimit 활성화
public class ResilienceConfig {
}
Java
import org.springframework.resilience.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    // 최대 3회 재시도, 지수 백오프 적용 (기본값: 1초 간격, 지수 증가)
    @Retryable(maxAttempts = 3)
    public PaymentResult processPayment(PaymentRequest request) {
        return paymentGateway.charge(request);
    }
}
Java

@EnableResilientMethods를 선언한 설정 클래스가 있어야 @Retryable@ConcurrencyLimit이 동작한다. 기존 Spring Retry 라이브러리와 달리 패키지가 org.springframework.resilience.annotation으로 변경되었으며, 별도의 spring-retry 의존성이 필요 없다.

위 코드에서 @Retryable은 외부 결제 게이트웨이 호출이 실패할 경우 최대 3회까지 재시도한다. 기본 동작은 1초 간격의 지수 백오프이며, jitter가 자동 적용되어 thundering herd 문제를 완화한다. reactive 메서드에 대해서도 자동으로 적응하므로 WebFlux 환경에서도 동일하게 사용할 수 있다.

@ConcurrencyLimit으로 동시 실행 제어

import org.springframework.resilience.annotation.ConcurrencyLimit;
import org.springframework.stereotype.Service;

@Service
public class ReportService {

    // 동시에 최대 5개의 리포트 생성 요청만 처리한다
    @ConcurrencyLimit(5)
    public Report generateReport(ReportRequest request) {
        // CPU 집약적인 리포트 생성 로직
        return reportEngine.generate(request);
    }
}
Java

@ConcurrencyLimit은 메서드의 동시 실행 수를 제한한다. Virtual Thread 환경에서 쓸모가 많은데, Virtual Thread는 수십만 개가 동시에 뜰 수 있지만 DB 커넥션 풀이나 외부 API는 그만큼 받아주지 못하기 때문이다. 위 예시에서 리포트 생성 요청이 5개를 넘으면 초과분은 대기 상태에 놓인다.

JSpecify로 null 안전성이 표준화되다

Spring이 자체 @Nullable을 쓰던 시절은 끝났다. Spring Framework 7은 JSpecify 표준 어노테이션으로 전환했으며, Spring 포트폴리오 전체에 걸쳐 타입, 제네릭, 배열, vararg 요소의 null 처리를 통일했다.

import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NonNull;

@NonNull  // 클래스 레벨: 모든 파라미터와 반환값이 기본적으로 non-null
public class UserService {

    // name 파라미터만 null 허용으로 명시적 선언
    public User findByName(@Nullable String name) {
        if (name == null) {
            return User.anonymous();
        }
        return userRepository.findByName(name);
    }
}
Java

@NonNull@Nullable 모두 org.jspecify.annotations 패키지로 바뀌었다. IntelliJ IDEA 2025.3부터 JSpecify 기반 데이터 흐름 분석을 지원하고, Kotlin 2는 이 어노테이션을 nullable/non-null 타입으로 자동 변환한다. 빌드 타임에 NullAway를 연동하면 null 관련 버그를 컴파일 시점에 잡을 수 있다.

마이그레이션, 무엇을 체크해야 하나

Spring Boot 4로의 업그레이드를 계획하고 있다면 먼저 Spring Boot 3.5로 올린 뒤 4.0으로 전환하는 단계적 접근을 권장한다. 공식 마이그레이션 가이드에서 전체 변경사항을 확인할 수 있다.

Spring Boot 4.0 마이그레이션 가이드 — 3.x에서 4.0으로 안전하게 올리는 법 참고

최소 요구사항

JDK            : 17 이상 (JDK 25 기능 지원)
Jakarta EE     : 11 (Servlet 6.1, Persistence 3.2)
Kotlin         : 2.2 이상
Gradle         : 9 이상
GraalVM        : native-image v25 이상
Plaintext

삭제된 기능과 대체 방법

삭제된 항목대체 방법
Undertow 웹서버Tomcat 11.0 또는 Jetty 12.1 사용
@MockBean / @SpyBean@MockitoBean / @MockitoSpyBean 사용
실행 가능 JAR 스크립트Docker 컨테이너 또는 시스템 서비스 활용
spring.data.mongodb.*spring.mongodb.*로 프로퍼티 경로 변경
Spring Session HazelcastRedis 또는 JDBC 기반 세션 스토어 전환

Spring Boot 내부 패키지 이동

javaxjakarta 전환은 Spring Boot 3.0에서 이미 완료되었으므로 Spring Boot 3.x 사용자라면 추가 작업이 필요 없다. 다만 Spring Boot 4에서는 내부 클래스의 패키지 재배치가 이루어졌다.

// Spring Boot 3.x
import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.env.EnvironmentPostProcessor;

// Spring Boot 4 — 내부 패키지가 세분화되었다
import org.springframework.boot.bootstrap.BootstrapRegistry;
import org.springframework.boot.EnvironmentPostProcessor;
Java

BootstrapRegistryorg.springframework.boot.bootstrap 하위로 이동했고, EnvironmentPostProcessororg.springframework.boot.env에서 org.springframework.boot로 위치가 변경되었다. 이러한 내부 API를 직접 사용하는 프로젝트라면 import 경로를 갱신해야 한다.

의존성 주요 업그레이드

<!-- Spring Boot 4에서 관리하는 주요 의존성 버전 -->
<!-- Hibernate ORM 7.1 — detached 엔티티 재연결(reattach) 불가 -->
<!-- Tomcat 11.0 / Jetty 12.1 -->
<!-- Kafka 4.1.0 / MongoDB Driver 5.6.0 -->
<!-- Mockito 5.20 / TestContainers 2.0 -->
XML

Hibernate 7.1에서는 detached 상태의 엔티티를 merge() 없이 직접 재연결하는 것이 불가능해졌다. 기존에 Session.update()로 detached 엔티티를 다시 영속 상태로 만들던 코드가 있다면 EntityManager.merge() 호출로 변경해야 한다. 또한 Hibernate의 annotation processor 의존성이 hibernate-jpamodelgen에서 hibernate-processor로 이름이 바뀌었다.

OpenTelemetry가 기본 탑재되다

Spring Boot 3.x에서 분산 트레이싱을 붙이려면 Actuator에 Micrometer Tracing을 조합하고, exporter를 별도로 설정해야 했다. Spring Boot 4는 spring-boot-starter-opentelemetry를 도입해서 이 과정을 줄였다. Actuator 없이도 트레이싱과 메트릭 수집이 가능하다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-opentelemetry</artifactId>
</dependency>
XML
# application.yml
management:
  otlp:
    tracing:
      endpoint: http://otel-collector:4318/v1/traces
    metrics:
      endpoint: http://otel-collector:4318/v1/metrics
YAML

의존성 하나에 OTLP 엔드포인트만 잡아주면 HTTP 요청, DB 쿼리, 메시지 처리에 대한 분산 트레이싱이 자동으로 수집된다. Micrometer Tracing + Zipkin 조합을 직접 구성하던 것과 비교하면 설정 파일 기준으로 20줄 이상이 줄어드는 셈이다.

마치며

Spring Boot 4는 API 버전 관리 내장, Jackson 3 전환, 스타터 모듈화, Resilience 통합, JSpecify null 안전성 등 상당히 많은 변화를 담고 있다. 업그레이드 작업량이 적지 않지만, 대부분의 애플리케이션에서는 pom.xml 의존성 변경과 일부 import 수정만으로 전환이 가능하다고 Spring 팀은 설명하고 있다. 다만 Jackson 커스텀 직렬화 코드가 많거나 Undertow를 사용하는 프로젝트라면 사전 검토가 필수적이다.

개인적으로 가장 반가운 기능은 API 버전 관리다. 매 프로젝트마다 URL 경로에 /v1/을 직접 넣거나 커스텀 어노테이션을 만들어 쓰던 경험이 있다 보니, 프레임워크 차원의 표준화된 지원이 이렇게 반가울 수가 없다. 특히 addDeprecatedVersions 한 줄로 RFC 표준 deprecation 헤더가 자동 발행되는 부분은 실무에서 클라이언트 팀과의 커뮤니케이션을 상당히 줄여줄 것으로 기대한다.

지금까지 Spring Boot 4 신기능에 대해서 정리해 보았다.