Java EE에서 Jakarta EE로: Spring Boot 3 마이그레이션 완전 가이드

javax.* 패키지가 jakarta.*로 바뀐 이유와 배경부터, Spring Boot 2 → 3 업그레이드 체크리스트, OpenRewrite를 활용한 자동 마이그레이션, 그리고 Security 6·Actuator 설정 변경점까지 실무 중심으로 정리합니다.

· 6 min read · PALDYN Team

지난 글에서 WebFlux의 함수형 엔드포인트를 살펴봤습니다. 이번 글부터는 현대 Spring 생태계를 다루는 파트로 넘어갑니다. 그 시작은 많은 팀이 한 번쯤 겪는 고통, 바로 Spring Boot 3 마이그레이션입니다.

왜 jakarta.* 로 바꿨나

2019년 Oracle이 Java EE 상표권과 javax.* 네임스페이스를 Eclipse Foundation에 이전하지 않기로 결정하면서, Eclipse는 새 브랜드 Jakarta EE 아래 패키지 이름을 바꿀 수밖에 없었습니다. 결과적으로 Jakarta EE 9부터 javax.* 대신 jakarta.* 를 사용합니다.

Spring Boot 3은 Jakarta EE 10을 기반으로 하며, 이로 인해 모든 javax.* 임포트를 jakarta.*로 변경해야 합니다. 단순한 문자열 치환처럼 보이지만, 대규모 프로젝트에서는 수백 개 파일에 걸친 작업이 됩니다.

javax에서 jakarta로 패키지 이름 변경 비교

마이그레이션 전제 조건

Spring Boot 3.x가 요구하는 최소 버전이 있습니다.

항목최소 버전
Java17
Spring Framework6.x
Hibernate ORM6.x
Tomcat10.x

Java 17 이전 버전을 사용하고 있다면 먼저 JDK 업그레이드가 필요합니다. 레코드, 텍스트 블록, 패턴 매칭 등 Java 17 기능을 적극 활용할 수 있으니 단순 비용이 아닌 투자로 볼 수 있습니다.

단계별 마이그레이션 체크리스트

마이그레이션 단계별 체크리스트

1단계: 의존성 버전 업그레이드

<!-- Maven pom.xml -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.5</version>
</parent>
// Gradle build.gradle
plugins {
    id 'org.springframework.boot' version '3.2.5'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'java'
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

2단계: javax.* 일괄 치환

수동으로 하면 실수가 납니다. OpenRewrite 플러그인이 가장 신뢰도 높은 자동화 도구입니다.

// build.gradle에 추가
plugins {
    id("org.openrewrite.rewrite") version "6.15.1"
}

dependencies {
    rewrite("org.openrewrite.recipe:rewrite-spring:5.x.x")
}

rewrite {
    activeRecipe(
        "org.openrewrite.java.spring.boot3.UpgradeSpringBoot_3_2"
    )
}
# 마이그레이션 실행
./gradlew rewriteRun

# 변경사항 미리보기 (실제 적용 없이 확인)
./gradlew rewriteDryRun

IntelliJ IDEA를 사용한다면 Refactor → Migrate to Jakarta EE 10 메뉴로도 자동 변환이 가능합니다.

3단계: 자주 바뀌는 패키지 확인

// Before (Spring Boot 2)
import javax.servlet.http.HttpServletRequest;
import javax.persistence.Entity;
import javax.validation.constraints.NotNull;
import javax.annotation.PostConstruct;

// After (Spring Boot 3)
import jakarta.servlet.http.HttpServletRequest;
import jakarta.persistence.Entity;
import jakarta.validation.constraints.NotNull;
import jakarta.annotation.PostConstruct;

javax.sql.*, javax.naming.*, javax.crypto.* 같이 Java SE 표준 라이브러리에 속하는 패키지는 바뀌지 않습니다. javax. 전체를 기계적으로 치환하면 안 됩니다.

Spring Security 6 주요 변경점

Security도 대규모 API 변경이 있었습니다.

// Spring Boot 2 방식 (더 이상 동작 안 함)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/public/**").permitAll()
            .anyRequest().authenticated();
    }
}
// Spring Boot 3 방식 (컴포넌트 기반)
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http)
            throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .formLogin(Customizer.withDefaults());
        return http.build();
    }
}

WebSecurityConfigurerAdapter가 완전히 제거됐습니다. 상속 대신 SecurityFilterChain 빈을 직접 선언하는 방식으로 전환해야 합니다. antMatchers 대신 requestMatchers를 사용합니다.

Actuator 기본값 변경

Spring Boot 3에서 Actuator 엔드포인트 기본 노출 정책이 바뀌었습니다.

# Spring Boot 3 기본값: info와 health만 HTTP 노출
# 필요한 엔드포인트를 명시적으로 추가
management:
  endpoints:
    web:
      exposure:
        include: "health,info,metrics,prometheus"
  endpoint:
    health:
      show-details: when-authorized

마이그레이션 후 검증

빌드만 통과했다고 끝이 아닙니다. 다음 항목을 반드시 확인하세요.

# 1. 단위 테스트 전체 실행
./gradlew test

# 2. javax 잔여 임포트 검색
grep -r "import javax\." src/main/java/ \
  --include="*.java" | grep -v "javax.sql\|javax.naming\|javax.crypto"

# 3. 빈 생성 실패 여부 확인 (통합 테스트)
./gradlew bootRun

트러블슈팅 자주 보는 오류

ClassNotFoundException: javax.servlet.http.HttpServletRequest
→ Tomcat 의존성이 이전 버전으로 고정된 경우입니다. 명시적 버전 지정을 제거하거나 BOM 버전을 확인하세요.

HibernateJpaDialect.convertHibernateAccessException
→ Hibernate 6.x에서 내부 예외 클래스 구조가 바뀌었습니다. 커스텀 예외 변환기가 있다면 재검토가 필요합니다.

IllegalStateException: No SecurityFilterChain found
WebSecurityConfigurerAdapter 방식을 제거했지만 새 방식의 SecurityFilterChain 빈을 등록하지 않은 경우입니다.


지난 글: 함수형 엔드포인트: RouterFunction과 HandlerFunction

다음 글: GraalVM 네이티브 이미지: Spring AOT 컴파일 완전 가이드


읽어주셔서 감사합니다. 😊