티스토리 뷰
0. 사전 준비 사항
[ SpringBoot 3.X 마이그레이션 사전 준비 사항 ]
- 2.7.x 최신 버전으로 사전 업데이트하기
- 버전업에 따른 의존성 변경 검토하기
- Spring Security 준비하기
- 스프링 부트 3에서는 스프링 시큐리티 6을 사용함
- 스프링 시큐리티는 6으로의 안전한 전환을 위해 5.8 버전을 제공하므로, 먼저 5.8로 버전업 필요
- 스프링 시큐리티 6은 모든 dispatch type에 대해 authorization을 적용하는데, 해당 타입을 지정하려면 spring.security.filter.dispatcher-types 프로퍼티를 사용하면 됨
- 시스팀 요구사항
- 자바 17 (자바 8은 더 이상 지원하지 않음)
- 스프링 프레임워크 6
[ IntelliJ 업데이트 ]
스프링 부트3는 스프링 버전6을 기반으로 하며, 스프링 6은 자바 최소 버전으로 자바 17을 요구하고 있다.하지만 스프링 부트 3.2부터 지원하는 가상 스레드(Virtual Thread)와 같은 기술을 사용하려면 자바 21이 되어야 한다. 따라서 가능하면 자바 버전도 21로 최신화하는 것이 좋다.
인텔리제이의 경우 2023.3 버전부터 자바 21의 지원이 시작되었다. 따라서 인텔리제이 버전이 2023.3 버전 미만이라면 이번 기회에 함께 업데이트 해주도록 하자.
[ Gradle 8.7 및 Java 21 업데이트 ]
그레이들의 경우 8.4부터 자바 21을 지원하고 있다. 따라서 그레이들 버전이 8.4 미만이라 그레이들 버전을 먼저 올려주도록 하자.
다음의 명령어를 통해 손쉽게 그레이들 버전을 높일 수 있다. 참고로 이 포스팅을 작성하는 시점에서의 최신 버전은 8.8이다. 참고로 그레이들 버전 8에서는 성능 향상 및 컴파일 최적화 등을 통해 개선되었다.
./gradlew wrapper --gradle-version=8.7
[ 일부 라이브러리 선 마이그레이션 ]
자바 21로 버전업을 하면서 일부 라이브러리의 버전을 먼저 올려줄 필요가 있다.
대표적으로 먼저 롬복의 경우 1.18.32를 사용해야 한다.
implementation("org.projectlombok:lombok:1.18.32")
또한 Kotest 등 bytebuddy에 의존중인 라이브러리를 사용중이라면 bytebuddy 버전을 직접 명시해서 의존성을 추가해주어야 한다. 왜냐하면 스프링 2.7 기준으로 bytebuddy 의존 버전은 1.12인데, 해당 버전은 자바 21을 지원하지 않기 때문이다.
참고로 스프링 부트 3.1부터는 byte-buddy 의존성이 1.14 버전으로 높아졌으므로, 버전업을 위해 임시로 추가했다가 지워주도록 하자.
// kotest가 의존하는 byte-buddy는 1.12 버전인데, java21을 지원하지 않음
implementation("net.bytebuddy:byte-buddy:1.14.17")
implementation("net.bytebuddy:byte-buddy-agent:1.14.17")
1. SpringBoot 3.0 마이그레이션
[ 스프링 프레임워크 6.0 및 부트 3.0 마이그레이션 ]
스프링 부트 3.0은 스프링 프레임워크 6.0을 기반으로 한다. 스프링 부트를 사용하면 버전 관리가 자동으로 되므로, 직접 스프링 프레임워크의 버전을 올려줄 필요는 없지만 어떠한 변화가 있었는지 자세히 살펴볼 필요가 있다. 스프링6 마이그레이션 가이드을 보면 주목할 부분이 상당히 많은데, 크게 다음과 같다.
javax 관련 패키지가 jakarta로 변경됨
JSR-330 애노테이션(@PostConstruct, @PreDestroy) 들을 포함하여 javax로 시작하는 패키지들이 jakarka로 변경(jakarta.*)되었다. 따라서 해당 부분을 마이그레이션해주어야 한다. 참고로 인텔리제이가 javax → jakarta 패키지 변경 기능을 제공해주므로, 이를 활용해주도록 하자.
LocalVariableTableParameterNameDiscoverer이 Deprecated됨
스프링에서 파라미터의 이름을 가져오는 방법이 여러 가지 있는데, 그 중에서 LocalVariableTableParameterNameDiscoverer는 LocalVariableTable이라는, 자바 바이트코드가 메서드의 로컬 변수 정보를 저장하는 테이블을 통해 값을 반환한다.
문제는 해당 기능이 Deprecated 되었다는 점인데, 관련 히스토리를 찾아보면 Native Image(네이티브 이미지)를 지원하기 위한 것으로 보인다. 따라서 이에 대한 대안으로 StandardReflectionParameterNameDiscoverer이 사용되기 시작했다.
StandardReflectionParameterNameDiscoverer는 리플렉션을 통해 파라미터 이름을 가져오는 기술이다. 문제는 리플렉션으로부터 해당 값을 가져오려면 컴파일 시에 -parameters 옵션을 반드시 추가해주어야 한다.
만약 Gradle 기반으로 빌드 및 배포를 한다면 -parameters 옵션을 추가하지 않아도 정상적으로 처리되는데, 그 이유는 Gradle 플러그인과 SpringBoot의 추가적인 설정 덕분이다. 자세한 내용은 이전 포스팅을 참고하도록 하자.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.6'
id 'io.spring.dependency-management' version '1.1.3'
}
@Async 메서드는 void 또는 Future 타입을 반환하도록 강제됨
비동기 처리를 위해 @Async를 사용하는 경우에는 이제 반환 타입이 void 또는 Future 타입이 되도록 강제되었다. 해당 조건을 충족시키지 못하는 코드가 있다면 에러가 발생할 것이다.
unique 빈 등록 메서드 이름 사용
스프링 6.0부터는 빈 등록 시에 유니크한 메서드 이름의 사용을 권장하고 있다. 만약 중복되는 빈 등록 메서드가 존재한다면, 다음과 같은 에러가 발생하게 된다. 따라서 빈 등록 메서드 이름을 모두 유니크하게 수정해주도록 하자.
org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem:
@Configuration class 'DataSourceConfiguration' contains overloaded @Bean methods with name 'dataSource'.
Use unique method names for separate bean definitions (with individual conditions etc)
or switch '@Configuration.enforceUniqueMethods' to 'false'.
컨트롤러 빈 등록 동작 변경
더 이상 컨트롤러에 @RequestMapping만 존재하는 클래스들이 컨트롤러 빈으로 등록되지 않는다. 이는 단순히 컨트롤러의 빈 인식 문제가 아니라, 컨트롤러에 인터페이스 기반 AOP 프록시가 동작하지 않음을 의미한다.
따라서 이러한 컨트롤러에 프록시 적용이 필요하다면 클래스 기반 AOP 프록시를 활성화해주거나, 인터페이스에 @Controller 관련 애노테이션을 명시해주어야 한다.
@RequestMapping("/health")
class SystemController {
@GetMapping
fun health(): Boolean {
return true
}
}
HttpMethod가 Enum에서 클래스로 변경됨
HttpMethod가 Enum에서 클래스로 변경되었다. 이로 인해 HttpMethod.values() 등에 접근하는 코드가 있다면 컴파일 에러가 발생할 수 있다.
Logging Date Format 변경
Logback과 Log4J2의 기본 로깅 날짜 포맷이 ISO-8601 표준 형태 yyyy-MM-dd’T’HH:mm:ss.SSSXXX 로 맞춰졌다. 따라서 date와 time 정보를 구분하기 위해 중간에 T가 들어가고, 끝에 타임존 정보가 들어갈 예정이다. LOG_DATEFORMAT_PATTERN 환경변수 또는 logging.pattern.dateformat 프로퍼티로 yyyy-MM-dd HH:mm:ss.SSS 같이 설정하면 이전 버전으로 맞출 수 있다.
logging.pattern.dateformat=yyyy-MM-dd HH:mm:ss.SS
@ConstructorBinding이 타입 수준에서 사용될 수 없음
기존에는 생성자를 통한 프로퍼티 바인딩을 위해 타입 레벨또는 생성자에 @ConstructorBinding을 붙여주어야 했다.
@ConstructorBinding
@ConfigurationProperties("sftp.config")
class SftpConfig(
val host: String,
val port: Int,
val user: String,
val password: String
)
하지만 이제 @ConstructorBinding이 없어도 기본적으로 생성자를 이용해서 프로퍼티가 바인딩된다. 만약 여러 개의 생성자가 존재할 경우에만 @ConstructorBinding를 붙여주면 된다.
@ConfigurationProperties("sftp.config")
class SftpConfig(
val host: String,
val port: Int,
val user: String,
val password: String
)
URL 매칭 관련 변화
기존에는 다음의 컨트롤러에서 "GET /some/greeting" 요청과 "GET /some/greeting/” 모두 매칭되었다.
@RestController
public class MyController {
@GetMapping("/some/greeting")
public String greeting() {
return "Hello";
}
}
하지만 스프링 프레임워크 변경에 따라 "GET /some/greeting/” 요청은 더 이상 매칭되지 않는다. 만약 하위 환성이 필요하다면 다음과 같은 설정을 통해 이를 적용시킬 수 있다.
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(true);
}
}
Actuator 관련 변경
- 기본 웹 엔드포인트 노출과 맞춰서, 이제 health 관련 엔드포인트들만 JMX를 통해 노출됨
- management.endpoints.jmx.exposure.include 또는 management.endpoints.jmx.exposure.exclude 속성으로 변경할 수 있음
- httptrace 엔드포인트가 httpexchanges로 rename됨
- actuator 엔드포인트 응답을 내려주기 위해 독립된 ObjectMapper가 사용됨
- 일관된 응답을 내려주기 위해 ObjectMapper를 독립시켰음
- management.endpoints.jackson.isolated-object-mapper 프로퍼티를 false로 설정하면 기존의 ObjectMapper를 사용할 예정
- Micrometer의 JvmInfoMetrics이 자동 구성됨
MySQL 드라이버 변경
스프링 부트 3부터 사용하는 드라이버가 mysql:mysql-connector-java에서 com.mysql:mysql-connector-j으로 변경되었다. 따라서 의존성을 변경해주도록 하자.
참고로 mysql-connector-j 8.0.23부터는 Timestamp와 OffsetDateTime의 경우 UTC로 timezone을 변환하는 기능이 default로 들어간다. 왜냐하면 preserveInstants=true인 경우 timestamp 변경을 하는데, 8.0.23 버전부터 해당 값이 기본적으로 true가 되기 때문이다.
따라서 mysql 서버의 timezone이 Asia/Seoul 로 설정되어 있더라도 UTC로 보정되어 의도치 않게 동작할 수 있다. 따라서 이를 방지하려면 spring.jpa.properties.hibernate.type.preferred_instant_jdbc_type=TIMESTAMP 으로 설정을 해주도록 하자.
Gradle 관련 변경
Gradle의 경우 mainClass 클래스를 손쉽게 지정할 수 있게 되었다.
springBoot {
mainClass = "com.example.Application"
}
또한 bootJar의 Layer 설정 역시 다음과 같이 활성화해줄 수 있다.
tasks.named<BootJar>("bootJar") {
layered {
enabled.set(false)
}
}
[ Configuration Properties 마이그레이션 ]
properties-migrator 지원
스프링 부트 3.0이 되면서, 많은 구성 프로퍼티들(Configuration Properties)이 삭제되었거나 이름이 변경되었다.
예를 들어 기존에 server.max-http-header-size 프로퍼티는 Tomcat에서는 최대 HTTP 요청/응답 크기를 설정했지만, Jetty나 Netty 또는 Undertow에서는 최대 HTTP 요청 크기만을 설정하였다. 따라서 이러한 불일치를 해결하고자 해당 프로퍼티를 deprecated하고 server.max-http-header-size 프로퍼티를 추가하였다.
이러한 변경사항이 매우 많이 존재하므로 application.properties 또는 application.yml에서 이를 수정해야 한다. 이를 일일이 탐지하는 것이 어려우므로, 스프링 부트는 진단 결과 분석 및 런타임 마이그레이션을 도와주는 라이브러리를 제공한다.
따라서 다음과 같은 의존성을 주입하여 사용해주도록 하자. 스프링 부트 공식 문서에서는 마이그레이션이 끝난 후에 해당 의존성을 반드시 제거하라고 명시하고 있다. 따라서 작업 후에 제거까지 마무리해주도록 하자.
runtimeOnly("org.springframework.boot:spring-boot-properties-migrator")
위의 도구를 사용하면 변경된 프로퍼티에 대해 다음과 같은 에러 로그를 남겨준다.
2024-06-20 18:47:51.786 ERROR 75550 --- [ main] o.s.b.c.p.m.PropertiesMigrationListener :
The use of configuration keys that are no longer supported was found in the environment:
Property source 'Config resource 'class path resource [config/application.properties]' via location 'optional:classpath:/config/'':
Key: management.trace.http.enabled
Line: 13
Reason: Replacement key 'management.httpexchanges.recording.enabled' uses an incompatible target type
Key: spring.mvc.throw-exception-if-no-handler-found
Line: 5
Reason: DispatcherServlet property is deprecated for removal and should no longer need to be configured
spring.data 프로퍼티 변경
스프링에서 spring.data prefix는 spring-data 용도로 예약(reserved)되었으며, 이는 클래스 패스에 spring-data 관련 의존성이 필요함을 의미한다. 따라서 해당 스펙에 맞게 기존의 프로퍼티들이 일부 변경되었다.
예를 들어 spring.redis 프로퍼티는 spring.data.redis 프로퍼티로 변경되었는데, 이는 RedisAutoConfiguration이 클래스패스에 Spring Data 의존성을 필요로 하기 때문이다.
[ Hibernate 6.1 마이그레이션 (Hibernate 6.1 Migration) ]
스프링부트 3부터 의존하는 하이버네이트(org.hibernate.orm) 버전이 5에서 6로 높아졌고, 스프링 부트 3.0은 하이버네이트 6.1 버전에 의존한다. 그에 따른 변화들이 일부 존재하는데, 해당 내용을 반영해주도록 하자.
시퀀스(sequence)와 테이블 명(table name) 생성 방식의 변경
먼저 식별자 생성(identifier generation)과 관련하여 시퀀스(sequence)와 테이블 명(table name)을 결정하는 방식이 있다. 하이버네이트 6부터는 default로 단일한 hibernate_sequence로 관리되는 것이 아니라 엔티티 계층 별로 고유한 sequence가 생성된다. 이로 인해 기존에 @GeneratedValue(strategy = GenerationStrategy.AUTO) 또는 단순히 @GeneratedValue 로 사용하던 애플리케이션의 경우, 각각의 엔티티 마다 <entity name>_seq 가 존재하는 것으로 설정된다. 예를 들어 Person 엔티티의 경우, person_seq이 존재할 것으로 기대한다.
따라서 이러한 변화로 인해 sequence가 존재하지 않는다면 에러가 발생할 수 있다. 하위 호환성을 위해 하이버네이트는 db_structure_naming_strategy 설정을 제공한다. 따라서 해당 값을 다음과 같이 설정하면 문제를 해결할 수 있다.
hibernate.id.db_structure_naming_strategy=single
hibernate.id.db_structure_naming_strategy=legacy
따라서 만약 다음과 같이 Table <Entity_SEQ> doesnt’t exists 에러가 발생한다면 해당 설정을 확인해주도록 하자.
org.springframework.dao.InvalidDataAccessResourceUsageException: error performing isolated work [Table 'toss_card.ads_card_apply_draft_SEQ' doesn't exist] [n/a]; SQL [n/a]
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:277)
at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:241)
at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:550)
at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:335)
만약 다음과 같은 설정을 통해 엔티티의 ID 생성 방식을 IDENTITY로 강제하고 있을 수도 있다. 해당 옵션은 ID 생성 방식이 AUTO로 설정된 경우에, ID 생성 전략을 DB에 위임하도록 강제한다.
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.properties.hibernate.id.new_generator_mappings=false
ID 생성 전략에 대한 도식화된 그림을 살펴보면 동작을 이해할 수 있다.
하지만 하이버네이트에서 해당 옵션을 제거했고 그에 따라 스프링 부트3 부터는 @GeneratedValue으로 사용하는 경우에 sequence 테이블이 반드시 필요하다. 만약 sequence 방식을 사용하지 않고, ID 생성 방식을 DB에 계속해서 위임할 것이라면 id 생성 방식을 명시해주도록 하자.
@Entity
@Table(name = "person")
class Person {
@Id
// ASIS: @GeneratedValue(strategy = GenerationStrategy.AUTO) 또는 @GeneratedValue
@GeneratedValue(strategy = GenerationType.*IDENTITY*)
val id: Long = 0L,
}
참고로 하이버네이트 팀의 개발자는 sequence를 사용하는 것이 대량의 데이터 처리에 더욱 적합하다고 설명하고 있으므로, 해당 부분을 인지해두기는 하도록 하자. 관련 내용은 여기서 참고해주도록 하자.
MySQLDialect 통합 및 Dialect 설정 제거
하이버네이트 6부터는 Dialect 관련한 변경이 있었다. 대표적으로 hibernate에서 지원하는 MySQL 관련 dialect가 MySQLDialect로 통합되었다.
[SpringBoot] Failed to initialize JPA EntityManagerFactory: Unable to create requested service [...]
due to: Unable to resolve name [org.hibernate.dialect.MySQL5InnoDBDialect] as strategy [...]
또한 최종적으로는 Dialect 설정을 하지 않아도 되도록 변경되었다. 따라서 Dialect를 명시중이라면 해당 부분을 제거해주도록 하자.
2024-06-24 18:24:38,168 WARN main (org.hibernate.orm.deprecation) [-:-] HHH90000025:
MySQLDialect does not need to be specified explicitly using 'hibernate.dialect'
(remove the property setting and it will be selected by default)
ClassNotFoundException: org.hibernate.dialect.PostgreSQL82Dialect
만약 MySQL을 사용중인데 다음과 같이 PostgreSQL82Dialect이 사용되려고 할 수 있다.
Caused by: java.lang.ClassNotFoundException: org.hibernate.dialect.PostgreSQL82Dialect
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
... 30 common frames omitted
그러면 라이브러리 중에 다음과 같이 함께 버저닝을 해주어야 하는 것들을 챙겨주도록 하자.
implementation("com.vladmihalcea:hibernate-types-55:2.21.1") // ASIS
implementation("com.vladmihalcea:hibernate-types-60:2.21.1") // TOBE
NoClassDefFoundError: javax/persistence/Transient
만약 부트 3를 사용하는데, jackson-datatype-hiberate에 의존한다면 다음과 같은 에러가 발생할 수 있다.
java.lang.RuntimeException: java.lang.NoClassDefFoundError: javax/persistence/Transient
at org.springframework.data.repository.core.support.RepositoryMethodInvoker$RepositoryFragmentMethodInvoker.lambda$new$0(RepositoryMethodInvoker.java:281)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.doInvoke(RepositoryMethodInvoker.java:170)
at org.springframework.data.repository.core.support.RepositoryMethodInvoker.invoke(RepositoryMethodInvoker.java:158)
따라서 다음의 의존성이 있는 프로젝트를 찾아서 exclude 해주도록 하자. 혹은 해당 의존성이 필요하다면 jakarta 의존성을 추가해주도록 하자.
exclude(group = "com.fasterxml.jackson.datatype", module = "jackson-datatype-hibernate5")
2. SpringBoot 3.1 마이그레이션
[ 스프링 부트 3.1 마이그레이션 ]
스프링 부트 3.1까지도 스프링 프레임워크 6.0을 기반으로 구동된다. 의존하는 마이너 버전에는 약간 차이가 있지만, 크게 동작 차이는 없으므로 스프링 부트 3.1 마이그레이션 사항만 살펴보도록 하자.
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.1-Release-Notes
필터 빈 등록 실패 케이스
기존에 필터(ServletRegistrationBean 과 FilterRegistrationBean) 빈 등록에 실패하면 WARN 로그를 남기고 있었다. 하지만 스프링 부트 3.1부터는 IllegalStateException 예외를 발생시키도록 변경되었으며, 만약 하위 호환성을 위해 설정이 필요하다면 해당 값을 true로 설정해주도록 하자.
setIgnoreRegistrationFailure(true)
RestTemplate의 Apache HttpClient4 지원 중단
RestTemplate의 Apache HttpClient4 지원이 중단되었다. 스프링 부트 3.0에서는 버전 관리 대상에도 HttpClient4와 HttpClient5가 모두 존재했지만, 이제 더 이상 HttpClient4 기반으로 RestTemplate을 생성할 수 없으므로 자동 버전 관리 대상에서도 제거되었다. 추가로 만약 HttpClient4를 계속 사용하려고 한다면 RestTemplate에서 찾기 힘든 에러가 발생할 것이다.
의존 라이브러리 버전 변경
- Apache HttpClient4 삭제
- testcontainers 추가
- Hibernate 6.2
- Jackson 2.15
- Mockito 5 with 5.3
[ Hibernate 6.2 마이그레이션 (Hibernate 6.2 Migration) ]
스프링 부트 3.0에서는 하이버네이트 6.1에 의존하지만, 스프링 부트 3.1에서는 하이버네이트 6.2에 의존한다. 6.1에서 6.2로의 변경 역시 참고해야 하는 부분이 있는데, 해당 내용을 살펴보도록 하자.
OffsetTime 매핑 변경(timezone 관련 변경)
OffsetTime의 값이 이제 @TimeZoneStorage과 hibernate.timezone.default_storage으로 결정된다. hibernate.timezone.default_storage의 기본값은 default이며, @TimeZoneStorage 값이 있을 경우 해당 값이 반영된다. 그리고 hibernate.timezone.default_storage으로 기본값이 설정되므로 OffsetTime을 이용하는 OffsetDateTime 또는 ZonedDateTime을 사용하는 경우, 해당 컬럼에 대한 DDL 변경이 예상될 수 있다.
@TimeZoneStorage 또는 hibernate.timezone.default_storage을 통해 TimeZoneStorageType이 결정되는데, 기본적으로 DEFAULT에 해당하며 이는 다음과 같다.
- DB가 timezone을 지원하는 경우
- NATIVE 방식으로 동작함
- NATIVE는 with time zone SQL 컬럼 타입과 함께 timezone을 저장함(SqlTypes.TIME_WITH_TIMEZONE)
- 이로 인해 스키마 검증에 실패할 수 있음
- DB가 timezone을 지원하지 않는 경우
- NORMALIZE_UTC 방식으로 동작함
- NORMALIZE_UTC은 timezone 정보를 저장하지 않지만, 시간 정보를 UTC로 보정함
하위 호환성을 위해 이전과 동일하게 동작하기를 원한다면 다음의 설정을 추가해주면 된다.
hibernate.timezone.default_storage=NORMALIZE
Boolean 및 Enum 타입인 경우 DDL 시에 constraint 조건 추가
Hibernate 6.2부터 Boolean 또는 Enum인 경우에 constraint 조건이 추가된다. 기존에는 타입에 대한 제약조건이 생성되지 않았는데, 이제부터 다음과 같이 check로 제약조건이 반영된다.
create table benefit_season (
id bigint generated by default as identity,
is_local varchar(255) check (is_local in ('N','Y')),
season varchar(255) check (season in ('SEASON1','SEASON2','SEASON3')),
)
3. SpringBoot 3.2 마이그레이션
[ 스프링 프레임워크 6.1 및 부트 3.2 마이그레이션 ]
스프링 부트 3.2부터는 의존하는 스프링 프레임워크의 버전이 6.1로 변경되었다. 따라서 스프링 부트 3.2와 스프링 프레임워크 6.1의 변경에 대해 살펴보도록 하자.
LocalVariableTableParameterNameDiscoverer이 제거됨
앞서 살펴보았던 그리고 스프링 6.0에서 deprecated 되었던 LocalVariableTableParameterNameDiscoverer 클래스가 6.1에서 최종 제거되었다. 따라서 이제 스프링 프레임워크를 포함하여 모든 스프링 관련 생태계에서는 더 이상 바이트코드를 파싱하여 매개변수 이름을 추론하려고 하지 않는다. 만약 의존관계 주입(DI, Dependency Injection), 프로퍼티 바인딩(property binding), SpEL 표현식 등에서 문제가 생겼다면 해당 이슈일 가능성이 높다. 따라서 parameters 옵션을 컴파일 시에 추가하는 방향으로 대응해주도록 하자.
유효성 검사를 위한 @Constraint 애노테이션 관련 기본 지원
추가적으로 Spring MVC와 WebFlux에서 @Constraint 관련 애노테이션을 기본적으로 지원하도록 개선되었다. 과거에는 컨트롤러 메서드에서 유효성 검사를 진행하려면, 다음과 같이 애노테이션을 붙여주어야 했다.
@RestController
class SystemController {
@GetMapping("/hello")
public Integer health(@RequestBody @Valid Name name){
return 0;
}
static class Name {
@NotBlank
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
하지만 스프링 6.1부터 MethodValidator 관련 기능을 제공하기 시작했고, 다음의 조건들이 충족된다면 기본적인 유효성 검증이 작동된다.
- 컨트롤러에 @Validated를 통한 AOP 기반 검증이 존재하지 않음
- LocalValidatorFactoryBean와 같은 jakarta.validation.Validator 타입의 빈이 등록됨
- 메서드 파라미터에 유효성 검증 애노테이션이 붙어있음
이로 인해 이제 유효성 검증이 ArgumentResolver가 아니라 메서드 레벨에서 동작하여 @Valid 애노테이션이 필요 없어졌고, 다음과 같이 @RequestParam에도 바로 유효성 검증을 적용할 수 있다.
@RestController
class SystemController {
@GetMapping("/hello")
public Integer health(@RequestParam @Length(min = 10) String name){
return 0;
}
}
@Autowiring 알고리즘 변화
Autowiring 시에 우선순위 관련 알고리즘 변경이 있었는데, 일반적으로는 문제되지 않을 상황이라 넘어가도 된다. 대신 ComponentScan 관련 개선 사항은 주목할만하다.
컴포넌트 스캐닝은 BeanFactory 초기화라는 상당히 이른 시점에 진행되는데, 이로 인해 늦게 평가되는 조건들에 의해서는 지켜지기 어려울 수 있다. 따라서 REGISTER_BEAN이라는 조건으로 빈을 등록한다면 보다 빠르게 평가되도록 개선되었다. 그 외에도 개선 사항들이 있었는데, 크게 중요하지 않은 것 같으므로 넘어가도록 하자.
의존 라이브러리 버전 변경
스프링 6.1에서 일부 라이브러리들의 최소 버전이 변경되었다. 따라서 해당 버전에 대한 관리를 해주도록 하자.
- SnakeYAML 2.0
- Jackson 2.14
- Kotlin Coroutines 1.7
- Kotlin Serialization 1.5
- H2 2.2
기타 여러 가지 개선 사항들
그 외에 ThreadPoolTaskExecutor 과 ThreadPoolTaskScheduler이 graceful shutdown 단계(phase)에 접어드는 시점이 애플리케이션 컨텍스트가 닫히기 시작하는 시점이 되었다. 따라서 해당 시점 이후에는 더 이상 새로운 task들을 받지 않는다. 만약 해당 처리가 필요하다면 acceptTasksAfterContextClose 옵션을 true로 주면 되는데, 이를 통해 종료 단계를 보다 길게 잡을 수 있다.
추가적으로 선언적인 Caching(Cache 애노테이션 관련) 기능 역시 개선된 부분이 있다. Mono, Flux와 같은 리액티브 타입을 반환하는 경우, 비동기 스트림을 캐싱하는 것이 아니라 처리한 값을 캐싱하도록 되었다. 이는 캐시 공급자(Cache Provider)로 하여금 추가적인 기능 구현을 필요로 했는데, setAsyncCacheMode를 설정함으로써 이러한 기능을 지원하게 되었다.
그 외에도 @TransactionalEventListener의 경우 동일한 메서드에서 유효하지 않은 @Transactional 사용에 대해 reject를 한다.
그것들 외에도 Preflight 검사가 인터셉터의 시작 시에 실행된다는 점이나 HttpInterface 클라이언트의 timeout 개선 및 Jackson의 ParameterNamesModule이 자동으로 등록되는 등 여러 가지 개선이 있었다.
스프링 부트 3.2 핵심 신규 기능 요약
- Virtual thread
- 기존의 자바 스레드는 OS 스레드의 래퍼라 상당히 무겁고 제약이 많았음
- 그래서 경량 스레드를 지원하게 되었고, 이를 통해 thread-per-request 프로그래밍 모델을 유지하면서도 상당한 처리량 향상을 얻을 수 있게 됨
- CRaC
- JVM의 체크포인트를 통해 빠르게 스냅샷 기반으로 애플리케이션을 띄우거나 복구할 수 있도록 함
- 예를 들어 JVM 위에 새로운 서버를 띄우면 warm-up을 통해 JIT 컴파일러 최적화가 필요한데, 이러한 문제들을 많이 해결할 수 있을듯
- SSL Bundle reloading
- SSL 관련해서 hot-reloading을 지원할 수 있도록 함
- RestClient
- WebClient의 Spring-MVC 버전으로 인터페이스가 완전히 동일함
- 기존의 webClient를 대체하면 이제 webmvc에서는 webflux 의존성을 가질 일이 거의 없을 듯
- JdbcClient
- WebClient처럼 DB 쪽에 유연한 기능을 사용할 수 있도록 해주는 도구
- JdbcTemplate을 래핑하여 보다 유연한 인터페이스를 제공함
참고 자료
- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0-Migration-Guide
- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.1-Release-Notes
- https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.2-Release-Notes
- https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x
- https://www.korecmblog.com/blog/upgrade-tospring6.1-parameter-name-retention
- https://techblog.lycorp.co.jp/ko/how-to-migrate-to-spring-boot-3