Spring Boot 4.0 마이그레이션 — Jakarta EE 11과 주요 변경점
Spring Boot 3.x로 겨우 마이그레이션을 끝냈는데, 또 메이저 버전이 올라간다고요?
Boot 2.x에서 3.x로 넘어갈 때 javax.* → jakarta.* 패키지 변경으로 고생했던 분이 많을 겁니다. Boot 4.0은 그때만큼 극적이진 않지만, Jakarta EE 11, Jackson 3.0, Hibernate 7.1 같은 굵직한 변경이 포함되어 있어서 미리 준비하지 않으면 빌드가 깨질 수 있습니다.
이 글에서는 Boot 4.0의 핵심 변경점을 정리하고, 실제 마이그레이션할 때 어떤 순서로 작업하면 좋을지 정리해 봤습니다.
Spring Boot 4.0이 중요한 이유
Boot 4.0은 Spring Framework 7 을 기반으로 합니다. 단순 버전 업이 아니라, 스프링 생태계의 근간이 되는 의존성들이 한꺼번에 올라가는 거라 영향 범위가 넓습니다.
| 항목 | Boot 3.x | Boot 4.0 |
|---|---|---|
| Spring Framework | 6.x | 7.x |
| Java 최소 버전 | 17 | 21 |
| Jakarta EE | 10 | 11 |
| Hibernate | 6.x | 7.1 |
| Jackson | 2.x | 3.0 |
왜 이렇게 한꺼번에 올리는 걸까요? 메이저 버전을 올릴 때가 아니면 하위 호환을 깨는 변경을 넣기 어렵기 때문입니다. 그래서 메이저 버전에 모아서 한 번에 정리하는 거죠.
1. Java 21 최소 요구
Boot 3.x는 Java 17이 최소였지만, 4.0은 Java 21 이상 을 요구합니다.
Java 21에서 추가된 기능들이 프레임워크 내부에서 적극 활용됩니다:
- Virtual Threads — 가벼운 스레드로 동시성 처리
- Record Patterns — 패턴 매칭 강화
- Sequenced Collections — 순서가 있는 컬렉션 인터페이스
- Switch Pattern Matching — switch 문에서 타입 패턴 사용
// Java 21의 Virtual Threads — Boot 4.0에서 기본 지원
// 별도 설정 없이도 활용 가능
@Bean
public TomcatProtocolHandlerCustomizer<?> protocolHandler() {
return handler -> handler.setExecutor(
Executors.newVirtualThreadPerTaskExecutor()
);
}
2. Jakarta EE 11 네임스페이스 변경
Boot 3.x에서 javax.* → jakarta.* 변경을 겪었다면, 이번엔 그 정도는 아닙니다. 패키지 이름 자체가 바뀌진 않고, 일부 API가 업데이트되거나 제거 됩니다.
주요 변경점
Jakarta EE 11에서 주목할 API 변경:
- Servlet 6.1: 비동기 처리 API 개선
- JPA 3.2: 새로운 쿼리 기능,
EntityManagerFactory개선 - Bean Validation 3.1: 새로운 제약 조건 추가
- CDI 4.1: 컨텍스트와 의존성 주입 개선
3. Jackson 3.0 — 패키지명이 바뀝니다
이건 꽤 영향이 큽니다. Jackson 3.0은 ** 패키지명이 변경 **되기 때문에 import문을 전부 수정해야 합니다.
// Before — Boot 3.x (Jackson 2.x)
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
// After — Boot 4.0 (Jackson 3.0)
import tools.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.annotation.JsonProperty; // 어노테이션은 유지
import tools.jackson.core.JsonProcessingException;
어노테이션 패키지(
com.fasterxml.jackson.annotation)는 그대로인데, 핵심 패키지(databind,core)는tools.jackson으로 바뀝니다. IDE의 일괄 치환 기능을 활용하되, 어노테이션 패키지는 건드리지 마세요.
Jackson 3.0의 다른 변경점도 있습니다:
// Jackson 3.0에서 ObjectMapper 생성 방식 변경
// Before — 기본 생성자 사용
ObjectMapper mapper = new ObjectMapper();
// After — Builder 패턴 권장
ObjectMapper mapper = JsonMapper.builder()
.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.build();
4. Hibernate 7.1
Hibernate도 메이저 버전이 올라가면서 몇 가지 중요한 변경이 있습니다.
// 1. 기존 Criteria API 일부 제거 — JPA 표준으로 통합
// Before (제거됨!)
Session session = entityManager.unwrap(Session.class);
Criteria criteria = session.createCriteria(User.class);
// After (JPA 표준 Criteria)
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
query.select(query.from(User.class));
// 2. @Type 어노테이션 변경
// Before — @Type(type = "json")
// After — @JdbcTypeCode(SqlTypes.JSON) // 타입 안전한 방식
5. HTTP Interface Client 개선
Boot 3.2에서 도입된 HTTP Interface Client가 4.0에서 더 강력해졌습니다.
// HTTP Interface Client — Boot 4.0 개선 사항
public interface UserClient {
// 기존 기능 — 선언적 HTTP 클라이언트
@GetExchange("/users/{id}")
User getUser(@PathVariable Long id);
// Boot 4.0 — 에러 핸들링 개선
@GetExchange("/users/{id}")
ResponseEntity<User> getUserWithStatus(@PathVariable Long id);
// Boot 4.0 — 리액티브 지원 강화
@GetExchange("/users")
Flux<User> getAllUsers();
// Boot 4.0 — 멀티파트 요청 지원
@PostExchange("/users/{id}/avatar")
void uploadAvatar(
@PathVariable Long id,
@RequestPart("file") Resource file
);
}
// 클라이언트 빈 등록 — HttpServiceProxyFactory로 프록시 생성
@Bean
public UserClient userClient(RestClient.Builder builder) {
RestClient restClient = builder.baseUrl("https://api.example.com").build();
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build()
.createClient(UserClient.class);
}
인터페이스만 정의하면 실제 HTTP 호출 코드를 스프링이 알아서 만들어 줍니다. Feign Client와 비슷한데, 스프링 네이티브로 제공되니까 별도 의존성이 필요 없습니다.
6. Deprecated API 대량 제거
Boot 4.0에서 가장 주의해야 할 부분입니다. Boot 3.x에서 deprecated 경고가 뜨던 API들이 ** 전부 제거 **됩니다.
// 제거되는 대표적인 API들
// 1. WebSecurityConfigurerAdapter — 이미 3.x에서 deprecated
// Before (제거됨!)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/**").authenticated();
}
}
// After — SecurityFilterChain 빈 방식
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/**").authenticated()
);
return http.build();
}
}
// 2. spring.factories 기반 자동 설정 — 완전 제거
// Before (META-INF/spring.factories) — 제거됨!
// org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
// com.example.MyAutoConfiguration
// After (META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)
// com.example.MyAutoConfiguration
7. Auto-Configuration 변경
@AutoConfiguration 어노테이션과 새로운 등록 방식이 ** 유일한 표준 **이 됩니다.
// Boot 4.0의 Auto-Configuration 표준 방식
@AutoConfiguration(
after = DataSourceAutoConfiguration.class, // 순서 지정
before = JpaRepositoriesAutoConfiguration.class
)
@ConditionalOnClass(DataSource.class)
@ConditionalOnProperty(
prefix = "app.feature",
name = "enabled",
havingValue = "true"
)
public class MyFeatureAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyFeatureService myFeatureService() {
return new MyFeatureService();
}
}
등록 파일 위치도 확인하세요:
# 파일 위치 (Boot 4.0에서 유일한 방식)
src/main/resources/META-INF/spring/
org.springframework.boot.autoconfigure.AutoConfiguration.imports
# 파일 내용 — 한 줄에 하나씩
com.example.MyFeatureAutoConfiguration
com.example.AnotherAutoConfiguration
spring.factories에EnableAutoConfiguration키로 등록하던 방식은 완전히 제거됩니다. 3.x에서 병행 지원하던 게 4.0에서 끊기는 거예요.
8. Virtual Threads — 일급 시민으로 승격
Boot 3.2에서 프리뷰로 제공되던 Virtual Threads 지원이 4.0에서 ** 기본 옵션 수준 **으로 격상됩니다.
# application.yml — Virtual Threads 활성화
spring:
threads:
virtual:
enabled: true # 이 한 줄이면 끝
이 설정 하나로 다음이 전부 Virtual Threads를 사용합니다:
- Tomcat 요청 처리 스레드
- @Async 태스크 실행
- @Scheduled 스케줄링
- Spring MVC 비동기 처리
Virtual Threads의 핵심은 ** 코드를 바꾸지 않고 동시성을 개선 **할 수 있다는 점입니다. 기존 Tomcat 플랫폼 스레드(기본 200개)에서 사실상 무제한 Virtual Threads로 바뀌니까, 동시 요청 처리량이 크게 올라갑니다.
다만 주의할 점이 있습니다:
// 주의 1: synchronized 대신 ReentrantLock 사용
// Before — Virtual Thread가 pin될 수 있음
public synchronized void updateCounter() {
this.counter++;
}
// After — pinning 방지
private final ReentrantLock lock = new ReentrantLock();
public void updateCounter() {
lock.lock();
try { this.counter++; }
finally { lock.unlock(); }
}
// 주의 2: ThreadLocal 대신 ScopedValue 고려
// Before — Virtual Thread마다 복사되어 메모리 낭비
private static final ThreadLocal<UserContext> context = new ThreadLocal<>();
// After — Java 21+ ScopedValue
private static final ScopedValue<UserContext> CONTEXT = ScopedValue.newInstance();
synchronized블록이나ThreadLocal남용이 있으면 오히려 성능이 떨어질 수 있으니, Virtual Threads를 켜기 전에 반드시 점검하세요.
마이그레이션 체크리스트
실제로 마이그레이션할 때는 이 순서로 진행하면 안전합니다.
Step 1: 사전 준비 (Boot 3.x 상태에서)
# deprecated 경고 전부 해결 — 이게 가장 중요합니다
./gradlew compileJava 2>&1 | grep -i "deprecated"
# 또는 Maven
mvn compile -Dmaven.compiler.showDeprecation=true
- Java 21 이상으로 업그레이드
- Boot 3.x 최신 버전으로 먼저 업데이트
- deprecated 경고 전부 해결
-
spring.factories→AutoConfiguration.imports로 전환
Step 2: 의존성 업그레이드
// build.gradle
plugins { id 'org.springframework.boot' version '4.0.0' }
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
- Boot 4.0으로 버전 변경
- 서드파티 라이브러리 호환성 확인 (QueryDSL, MapStruct, Lombok 등)
- 빌드 실행 → 컴파일 에러 확인
Step 3: Jackson 3.0 + Hibernate 7.1 대응
- Jackson import문 변경 (
com.fasterxml.jackson.databind→tools.jackson.databind) - 커스텀 Serializer/Deserializer 코드 수정
- Hibernate 전용 Criteria API → JPA 표준으로 교체
-
@Type→@JdbcTypeCode로 변경
Step 4: 테스트 및 검증
- 단위 테스트 전체 통과
- 통합 테스트 전체 통과
- 스테이징 환경에서 E2E 테스트
자주 하는 실수
공부하다 보니 마이그레이션에서 자주 놓치는 부분이 몇 가지 있었습니다.
- Jackson 어노테이션 패키지까지 바꿔버리는 실수 —
tools.jackson.annotation.JsonProperty는 존재하지 않습니다. 어노테이션은com.fasterxml.jackson.annotation그대로입니다. - Virtual Threads 켜놓고 synchronized 방치 — Thread Pinning으로 성능이 오히려 떨어집니다. ReentrantLock으로 교체하세요.
- ** 서드파티 호환성 무시** — QueryDSL, MapStruct, Lombok 같은 코드 생성 도구는 반드시 호환 버전을 확인하세요.
정리
Boot 4.0 마이그레이션의 핵심을 한 줄로 요약하면:
deprecated 경고를 먼저 해결하고, Jackson 패키지명과 Java 21을 챙기면 절반은 끝입니다.
3.x → 4.0은 2.x → 3.x처럼 패키지 이름이 통째로 바뀌는 수준은 아닙니다. 하지만 Jackson 3.0의 패키지 변경, Hibernate 7.1의 API 제거, deprecated API 정리 등이 겹치면서 손이 가는 작업이 꽤 됩니다.
미리 Boot 3.x 최신 버전에서 deprecated 경고를 전부 해결해두면, 실제 마이그레이션할 때 훨씬 수월합니다. 한 번에 하려고 하지 말고 3.x 정리 → 4.0 업그레이드 순서로 단계별로 진행하는 게 가장 안전합니다.