들어가며
Spring Boot 3.x가 릴리즈된 지 꽤 시간이 지났지만, 아직도 2.x 버전에서 운영 중인 프로젝트가 많습니다. 저 역시 실무에서 마이그레이션을 진행하면서 예상치 못한 부분에서 빌드가 깨지고, 런타임 에러가 터지는 경험을 했습니다. 이 글에서는 Spring Boot 3.x로 넘어갈 때 반드시 알아야 할 핵심 변경사항들을 정리합니다. 단순한 나열이 아니라, 실무에서 실제로 부딪히는 포인트 위주로 다루겠습니다.
1. Jakarta EE 전환 — javax에서 jakarta로
Spring Boot 3.x에서 가장 광범위하게 영향을 미치는 변경사항입니다. Java EE가 Eclipse Foundation으로 이관되면서 패키지 네임스페이스가 javax.*에서 jakarta.*로 변경되었고, Spring Boot 3.x는 Jakarta EE 9+ 기반으로 동작합니다.
영향 범위
javax.servlet.*→jakarta.servlet.*javax.persistence.*→jakarta.persistence.*javax.validation.*→jakarta.validation.*javax.annotation.*→jakarta.annotation.*javax.transaction.*→jakarta.transaction.*
단순히 import문만 바꾸면 될 것 같지만, 실제로는 그렇게 간단하지 않습니다. 서드파티 라이브러리가 아직 jakarta를 지원하지 않는 경우가 문제입니다.
실무 대응 코드
// Before (Spring Boot 2.x)
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.validation.constraints.NotBlank;
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
}
// After (Spring Boot 3.x)
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.validation.constraints.NotBlank;
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
private String name;
}
팁: IntelliJ에서 Ctrl+Shift+R (Replace in Files)로 javax.persistence를 jakarta.persistence로 일괄 치환할 수 있습니다. 단, javax.crypto나 javax.net.ssl 같은 Java SE 패키지는 변경 대상이 아니므로 주의해야 합니다. 정규식으로 javax\.(persistence|servlet|validation|annotation|transaction) 패턴을 사용하면 안전합니다.
2. Spring Security 설정 방식의 대격변
Spring Security 6.x(Spring Boot 3.x에 포함)에서 WebSecurityConfigurerAdapter가 완전히 제거되었습니다. 이전부터 Deprecated 상태였지만, 이제는 컴파일 자체가 되지 않습니다. SecurityFilterChain을 Bean으로 등록하는 컴포넌트 기반 설정 방식으로 전환해야 합니다.
Before — 상속 기반 설정 (Spring Boot 2.x)
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
After — Bean 등록 기반 설정 (Spring Boot 3.x)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
주의할 점이 몇 가지 있습니다.
authorizeRequests()→authorizeHttpRequests()로 변경antMatchers()→requestMatchers()로 변경- 메서드 체이닝의
.and()패턴 대신 Lambda DSL을 사용 csrf(),formLogin()등 각 설정도 Lambda 방식으로 전달
3. GraalVM Native Image 공식 지원
Spring Boot 3.x부터 GraalVM Native Image를 공식적으로 지원합니다. 이전에는 Spring Native라는 별도 프로젝트로 실험적 지원이었지만, 이제 Spring Boot 코어에 통합되었습니다. 컨테이너 환경에서 빠른 기동 시간과 낮은 메모리 사용량이 필요한 경우 강력한 선택지입니다.
Native Image 빌드 설정
// build.gradle (Gradle)
plugins {
id 'org.graalvm.buildtools.native' version '0.10.4'
id 'org.springframework.boot' version '3.4.1'
}
// Native Image 빌드 실행
// ./gradlew nativeCompile
<!-- pom.xml (Maven) -->
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<!-- 빌드: mvn -Pnative native:compile -->
실무에서의 주의사항
- 리플렉션 제한: Native Image는 빌드 시점에 모든 코드 경로를 분석합니다. 런타임 리플렉션을 사용하는 라이브러리는
reflect-config.json등의 힌트 파일이 필요합니다. - 빌드 시간: Native 컴파일은 일반 JVM 빌드 대비 수 분에서 십수 분 이상 걸립니다. CI/CD 파이프라인에서 별도 스테이지로 분리하는 것을 권장합니다.
- AOT 처리: Spring Boot 3.x의 AOT(Ahead-of-Time) 엔진이 빈 등록, 프로퍼티 바인딩 등을 빌드 시점에 미리 처리합니다.
@Profile이나@Conditional계열 어노테이션의 동작이 달라질 수 있으니 테스트를 반드시 거쳐야 합니다. - 적합한 케이스: 서버리스(AWS Lambda), 마이크로서비스에서 콜드 스타트가 중요한 환경에 적합합니다. 반면 장시간 구동되는 모놀리식 서버에서는 JIT 컴파일의 최적화가 더 유리할 수 있습니다.
4. @HttpExchange — 선언적 HTTP 클라이언트
Spring Boot 3.x(Spring Framework 6)에서 도입된 @HttpExchange는 인터페이스 기반의 선언적 HTTP 클라이언트입니다. 기존에 Feign Client를 사용하기 위해 Spring Cloud 의존성을 추가하던 것과 달리, Spring Framework 자체에서 유사한 기능을 제공합니다.
인터페이스 정의
public interface UserApiClient {
@GetExchange("/users/{id}")
UserResponse getUser(@PathVariable("id") Long id);
@GetExchange("/users")
List<UserResponse> getUsers(@RequestParam("page") int page);
@PostExchange("/users")
UserResponse createUser(@RequestBody UserCreateRequest request);
@DeleteExchange("/users/{id}")
void deleteUser(@PathVariable("id") Long id);
}
클라이언트 Bean 등록
@Configuration
public class ApiClientConfig {
@Bean
public UserApiClient userApiClient() {
WebClient webClient = WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(WebClientAdapter.create(webClient))
.build();
return factory.createClient(UserApiClient.class);
}
}
RestClient 기반 사용 (Spring Boot 3.2+)
@Configuration
public class ApiClientConfig {
@Bean
public UserApiClient userApiClient() {
RestClient restClient = RestClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build();
return factory.createClient(UserApiClient.class);
}
}
Spring Cloud 없이도 선언적 HTTP 클라이언트를 쓸 수 있다는 것이 큰 장점입니다. 다만 Feign이 제공하는 서킷브레이커 통합, 재시도 정책 같은 고급 기능이 필요하다면 여전히 Spring Cloud OpenFeign을 고려해야 합니다.
5. 실무 마이그레이션 체크리스트
마이그레이션을 진행할 때 아래 체크리스트를 순서대로 확인하시기 바랍니다. 실제 프로젝트에서 빠뜨리기 쉬운 항목 위주로 정리했습니다.
사전 준비
- Java 버전 확인: Spring Boot 3.x는 최소 Java 17을 요구합니다. JDK 업그레이드가 선행되어야 합니다.
- Spring Boot 2.7.x로 먼저 업그레이드: 2.5, 2.6에서 바로 3.x로 넘어가지 말고, 2.7.x를 거치는 것이 안전합니다. Deprecated API 경고를 먼저 해소할 수 있습니다.
- 서드파티 라이브러리 호환성 확인: QueryDSL, MapStruct, Lombok 등 주요 라이브러리의 Jakarta EE 지원 버전을 확인합니다.
코드 변경
javax.*→jakarta.*import 일괄 변환 (Java SE 패키지 제외)- Spring Security 설정을
SecurityFilterChainBean 방식으로 전환 spring.redis.*프로퍼티가spring.data.redis.*로 변경됨spring.elasticsearch.*프로퍼티가spring.elasticsearch.uris등으로 재구성됨@ConstructorBinding어노테이션 위치가 클래스 레벨에서 생성자 레벨로 변경- Trailing Slash 매칭이 기본적으로 비활성화됨 (
/api/users/와/api/users가 별개로 처리)
빌드 및 테스트
- Gradle 사용 시 7.x 이상 필요 (8.x 권장)
- 테스트 코드에서
javax.servlet.http.MockHttpServletRequest등도 jakarta로 변경 필요 - 통합 테스트에서 Tomcat 10.x 기반으로 동작하므로, 임베디드 서버 관련 설정 확인
운영 환경
- Actuator 엔드포인트 보안 설정 재검토 (Security 설정 변경에 따라 접근 권한이 달라질 수 있음)
- Micrometer Observation API로 메트릭/트레이싱 통합 — 기존 Spring Cloud Sleuth 대신 Micrometer Tracing 사용
- 배포 전 카나리 배포 또는 블루-그린 배포를 통해 점진적으로 트래픽 전환 권장
마치며
Spring Boot 3.x 마이그레이션은 단순한 버전 업그레이드가 아닙니다. Jakarta EE 전환이라는 Java 생태계의 근본적인 변화를 반영하는 작업이기 때문에, 충분한 테스트와 단계적 접근이 필수입니다. 특히 운영 중인 서비스라면 2.7.x를 경유지로 삼아 Deprecated 경고를 모두 해소한 뒤 3.x로 넘어가는 전략을 추천합니다.
이 글에서 다룬 내용 외에도 ProblemDetail 기반 에러 응답(RFC 7807), RestClient 도입, Observability 통합 등 다양한 변화가 있습니다. 하나의 글에 다 담기 어려운 만큼, 후속 포스팅에서 이어서 다루겠습니다.
'Spring Boot' 카테고리의 다른 글
| Spring Security 6 실전 가이드 - JWT + OAuth2 인증/인가 완벽 정리 (0) | 2026.04.02 |
|---|---|
| Spring AI 2.0과 Spring Boot 4 - AI 퍼스트 Java 개발의 시작 (0) | 2026.04.01 |
| Spring Boot 테스트 전략 - 단위 테스트부터 통합 테스트까지 (0) | 2026.03.31 |
| Spring AOP 실전 활용 - 로깅부터 성능 측정까지 (0) | 2026.03.26 |
| Spring Boot 예외 처리 전략 - @ControllerAdvice부터 ProblemDetail까지 (0) | 2026.03.26 |