Spring Boot 2.x에서 4.x로, Java 8에서 21로 업그레이드하면서 겪은 마이그레이션 경험을 정리했다.
개요
| 항목 |
변경 전 |
변경 후 |
| Java |
8 |
21 |
| Spring Boot |
2.3.0.RELEASE |
4.0.1 |
| Gradle |
6.9.4 |
9.1.0 |
| Kotlin |
미사용 |
2.2.0 |
| Jackson |
2.x |
3.0.0 |
Part 1: Gradle 업그레이드 (6.9.4 → 9.1.0)
gradle-wrapper.properties
# 변경 전
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.4-all.zip
# 변경 후
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip
Deprecated API 수정
compile → implementation/api
# 변경 전
compile 'org.example:library:1.0'
# 변경 후
implementation 'org.example:library:1.0' // 내부 사용
api 'org.example:library:1.0' // 외부 노출
classifier → archiveClassifier
# 변경 전
classifier = 'sources'
# 변경 후
archiveClassifier = 'sources'
layout.buildDirectory 사용
# 변경 전
outputDir = file("$buildDir/classes/main")
# 변경 후
outputDir = layout.buildDirectory.dir("classes/main").get().asFile
Part 2: Java 업그레이드 (8 → 21)
build.gradle 설정
java {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
Javadoc 링크 업데이트
javadoc {
options {
// 변경 전
links "https://docs.oracle.com/javase/8/docs/api/"
// 변경 후
links "https://docs.oracle.com/en/java/javase/21/docs/api/"
}
}
Javadoc 헤딩 규칙 (Java 21)
Java 21에서는 Javadoc 헤딩 계층 구조가 엄격해졌다.
// 변경 전 (오류 발생)
/**
* <h1>클래스 설명</h1>
*/
// 변경 후
/**
* <h2>클래스 설명</h2>
*/
<h1>은 클래스/메서드 이름에 예약되어 있으므로 <h2> 이하를 사용해야 한다.
의존성 업데이트
BouncyCastle
// 변경 전
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
// 변경 후
implementation 'org.bouncycastle:bcpkix-jdk18on:1.83'
MSSQL JDBC
// 변경 전
implementation 'com.microsoft.sqlserver:mssql-jdbc:9.4.0.jre8'
// 변경 후
implementation 'com.microsoft.sqlserver:mssql-jdbc:12.6.1.jre11'
Part 3: Spring Boot 업그레이드 (2.3.0 → 4.0.1)
루트 build.gradle
plugins {
// 변경 전
id 'org.springframework.boot' version '2.3.0.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
// 변경 후
id 'org.springframework.boot' version '4.0.1'
id 'io.spring.dependency-management' version '1.1.7'
}
javax → jakarta 마이그레이션
Spring Boot 3.0+에서는 Java EE (javax)가 Jakarta EE (jakarta)로 변경되었다.
패키지 변경
| 변경 전 |
변경 후 |
javax.servlet.* |
jakarta.servlet.* |
javax.validation.* |
jakarta.validation.* |
javax.persistence.* |
jakarta.persistence.* |
javax.mail.* |
jakarta.mail.* |
의존성 변경
// 변경 전
compileOnly 'javax.servlet:javax.servlet-api'
// 변경 후
compileOnly 'jakarta.servlet:jakarta.servlet-api'
// 변경 전
implementation 'com.sun.mail:javax.mail:1.6.2'
// 변경 후
implementation 'org.eclipse.angus:angus-mail:2.0.2'
Java 파일 수정
// 변경 전
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Cookie;
// 변경 후
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Cookie;
MyBatis 업그레이드
// 변경 전
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.0'
// 변경 후
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:4.0.1'
HTTP Message Converter
Spring Boot 4에서는 Jackson 3 기반 메시지 컨버터가 자동 설정된다.
// 변경 전 - 수동 Bean 등록
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
jsonConverter.setDefaultCharset(StandardCharsets.UTF_8);
return jsonConverter;
}
// 변경 후 - Bean 제거 (Spring Boot 자동 설정 사용)
// MappingJackson2HttpMessageConverter는 deprecated
Part 4: Spring Batch 6 마이그레이션
Spring Boot 4.0은 Spring Batch 6.0을 사용한다.
Deprecated 클래스 제거
| Deprecated (Batch 5) |
대체 (Batch 6) |
JobRepositoryFactoryBean |
JdbcDefaultBatchConfiguration |
TaskExecutorJobLauncher |
자동 구성 |
JobRegistrySmartInitializingSingleton |
자동 구성 |
JobRegistryBeanPostProcessor |
자동 구성 |
JdbcDefaultBatchConfiguration 사용
// 변경 전 (Spring Batch 5)
@Configuration
public class BatchConfig {
@Bean
public JobRepository jobRepository(DataSource dataSource,
PlatformTransactionManager transactionManager) throws Exception {
JobRepositoryFactoryBean factory = new JobRepositoryFactoryBean();
factory.setDataSource(dataSource);
factory.setTransactionManager(transactionManager);
factory.setIsolationLevelForCreate("ISOLATION_SERIALIZABLE");
factory.setMaxVarCharLength(2500);
factory.afterPropertiesSet();
return factory.getObject();
}
@Bean
public JobLauncher jobLauncher(JobRepository jobRepository) throws Exception {
TaskExecutorJobLauncher launcher = new TaskExecutorJobLauncher();
launcher.setJobRepository(jobRepository);
launcher.afterPropertiesSet();
return launcher;
}
}
// 변경 후 (Spring Batch 6)
@Configuration
public class BatchConfig extends JdbcDefaultBatchConfiguration {
private final DataSource batchDataSource;
private final PlatformTransactionManager batchTransactionManager;
public BatchConfig(@Qualifier("batchDataSource") DataSource batchDataSource,
@Qualifier("batchTransactionManager") PlatformTransactionManager batchTransactionManager) {
this.batchDataSource = batchDataSource;
this.batchTransactionManager = batchTransactionManager;
}
@Override
@Bean
public DataSource getDataSource() {
return this.batchDataSource;
}
@Override
@Bean
public PlatformTransactionManager getTransactionManager() {
return this.batchTransactionManager;
}
}
Isolation Level 설정 변경
// 변경 전 (Spring Batch 5)
import org.springframework.transaction.TransactionDefinition;
@Override
protected int getIsolationLevel() {
return TransactionDefinition.ISOLATION_REPEATABLE_READ;
}
// 변경 후 (Spring Batch 6)
import org.springframework.transaction.annotation.Isolation;
@Override
protected Isolation getIsolationLevelForCreate() {
return Isolation.REPEATABLE_READ;
}
Isolation 값 매핑
| TransactionDefinition (int) |
Isolation (enum) |
ISOLATION_DEFAULT |
Isolation.DEFAULT |
ISOLATION_READ_UNCOMMITTED |
Isolation.READ_UNCOMMITTED |
ISOLATION_READ_COMMITTED |
Isolation.READ_COMMITTED |
ISOLATION_REPEATABLE_READ |
Isolation.REPEATABLE_READ |
ISOLATION_SERIALIZABLE |
Isolation.SERIALIZABLE |
데이터베이스별 권장 설정
@Override
protected Isolation getIsolationLevelForCreate() {
String dbName = getDatabaseProductName();
if (dbName != null) {
if (dbName.toLowerCase().contains("microsoft sql server")) {
return Isolation.REPEATABLE_READ; // MSSQL
} else if (dbName.toLowerCase().contains("postgresql")) {
return Isolation.READ_COMMITTED; // PostgreSQL
}
}
return Isolation.SERIALIZABLE; // 기본값
}
@Override
protected int getMaxVarCharLength() {
String dbName = getDatabaseProductName();
if (dbName != null && dbName.toLowerCase().contains("microsoft sql server")) {
return 1250; // MSSQL은 2500 초과 시 오류 발생 가능
}
return super.getMaxVarCharLength(); // 기본값 2500
}
Part 5: Kotlin 추가 (2.2.0)
루트 build.gradle
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.2.0' apply false
id 'org.jetbrains.kotlin.plugin.spring' version '2.2.0' apply false
}
ext {
set('kotlinVersion', "2.2.0")
}
subprojects {
pluginManager.withPlugin('org.jetbrains.kotlin.jvm') {
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion}"
implementation "org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}"
}
compileKotlin {
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
}
}
compileTestKotlin {
compilerOptions {
freeCompilerArgs.add("-Xjsr305=strict")
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21)
}
}
}
}
서브모듈 build.gradle (Kotlin 사용 시)
plugins {
id 'org.jetbrains.kotlin.jvm'
id 'org.jetbrains.kotlin.plugin.spring'
}
Part 6: Jackson 3 마이그레이션 (2.x → 3.0.0)
의존성
dependencies {
// Jackson 3 databind
implementation 'tools.jackson.core:jackson-databind:3.0.0'
// Jackson annotations (2.20 버전 유지)
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.20'
// 테스트용 Jackson 2 datetime 지원
testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
}
패키지 변경
| 변경 전 |
변경 후 |
com.fasterxml.jackson.core.* |
tools.jackson.core.* |
com.fasterxml.jackson.databind.* |
tools.jackson.databind.* |
com.fasterxml.jackson.annotation.* |
com.fasterxml.jackson.annotation.* (유지) |
클래스명 변경
| 변경 전 |
변경 후 |
JsonSerializer<T> |
ValueSerializer<T> |
JsonDeserializer<T> |
ValueDeserializer<T> |
SerializerProvider |
SerializationContext |
ObjectMapper.builder() |
JsonMapper.builder() |
메서드 시그니처 변경
// 변경 전
@Override
public void serialize(T value, JsonGenerator gen, SerializerProvider provider) throws IOException {
}
// 변경 후 (throws IOException 제거 - unchecked exception 사용)
@Override
public void serialize(T value, JsonGenerator gen, SerializationContext ctxt) {
}
JsonNode API 변경
// 변경 전
root.fieldNames().
forEachRemaining(fieldName ->{
JsonNode node = root.get(fieldName);
if(node instanceof ArrayNode){}
if(node instanceof TextNode){}
});
// 변경 후
root.
properties().
forEach(entry ->{
String fieldName = entry.getKey();
JsonNode node = entry.getValue();
if(node.
isArray()){}
if(node.
isTextual()){}
});
기본값 변경
| 설정 |
Jackson 2 |
Jackson 3 |
| 날짜 직렬화 |
타임스탬프 (숫자) |
ISO-8601 문자열 |
| 속성 정렬 |
선언 순서 |
알파벳 순서 |
| Java 8 Time |
모듈 필요 |
기본 내장 |
속성 순서 유지
@JsonPropertyOrder({"id", "name", "createdAt", "updatedAt"})
public class User {
}
Part 7: 검증
빌드 확인
JAVA_HOME="/path/to/jdk21" ./gradlew clean build
테스트 실행
JAVA_HOME="/path/to/jdk21" ./gradlew test
javax 잔여 확인
./gradlew dependencies | grep javax
참고 자료
댓글