해당 포스트는 Meduim 의 아티클 "Spring Boot3 Migration" 을 번역한 내용이다.
[ 원문 ]
Spring Boot3 마이그레이션 준비
Spring-boot 2의 마지막 버전인 2.7.x 지원종료(2023년 11월 18일)가 다가오고 있어, Spring Boot 3 으로 마이그레이션하기 시작했고, 그동안 마주친 중요한 문제들을 기록했다.
- Java 17
- Jakarta EE
- Kafka
- OpenAPI
- Spring Security
수 많은 마이그레이션 가이드가 존재하지만, 다음의 두 문서에서 시작하는것을 추천한다.
Preparing for Spring Boot 3.0
Spring Boot 2.0 was the first release in the 2.x line and was published on Feburary 28th 2018. We’ve just released Spring Boot 2.7 which means that, so far, we’ve been maintaining the 2.x line for just over 4 years. In total we’ve published 95 distin
spring.io
Spring Boot 3.0 Migration Guide
Spring Boot. Contribute to spring-projects/spring-boot development by creating an account on GitHub.
github.com
스프링 부트3 마이그레이션 후, 프로젝트 종속성은 다음과 같을 것이다.
+----------------------+---------+
| Lib | Version |
+----------------------+---------+
| spring-boot | 3.0.6 |
| spring-core | 6.0.8 |
| spring-security-core | 6.0.3 |
| spring-webmvc | 6.0.8 |
| httpclient5 | 5.1.4 |
| spring-kafka | 3.0.6 |
| spring-kafka-test | 3.0.6 |
| kafka-clients | 3.3.2 |
+----------------------+---------+
Java 17
스프링3 에서 요구하는 자바의 최소버전은 Java17 이다.
시간 정밀도 (Instant precision)
Java 11 - Instant: 2022-12-12T18:04:27.267229Z
Java 17 - Instant: 2022-12-12T18:04:27.267229114Z
특정 플랫폼에서 Instant 는 9자리의 숫자 지원한다. 그러나, Postgres, MySQL, Kafka 는 여전히 6자리를 지원한다. 따라서, 일부 단위테스트에서 원본 타임스템프값과 변환된 값이 일치하지 않을 수 있다.
해결책
원본 타임스템프의 microseconds 를 잘라서 사용해라
Instant.now().truncatedTo(ChronoUnit.MICROS)
참고 자료
- Changes in Instant.now() between Java 11 and Java 17 in AWS Ubuntu standard 6.0
- Java 15 nanoseconds precision causing issues in the Linux environment
Jakarta EE
Spring Boot 3 릴리즈 노트에서는 Java 17 에서 javax 네임스페이스가 제거되었기 때문에, Jakarta EE 9 최소 요구 사항이라고 명시되어 있다.
해당 아티클에서는 모든 javax 를 jakarta 네임스페이스로 대체하는 것을 권고하고 있다.
javax.persistence.EmbeddedId
javax.persistence.Entity
javax.persistence.Lob
javax.persistence.Table
to
jakarta.persistence.EmbeddedId
jakarta.persistence.Entity
jakarta.persistence.Lob
jakarta.persistence.Table
의존성에는 javax 패키지 대신에 jakarta 패키지가 다음과 같이 존재해야 한다.
jakarta.persistence-api
jakarta.validation-api
jakarta.annotation-api
jakarta.servlet-api
Apache HttpClient in RestTemplate
Spring Framework 6.0 에서는 아파치 HttpClient 가 제거되고 org.apache.httpcomponents.client5:httpclient5 로 대체되었다. (참고 : 해당 dependency 는 groupId 가 다름). HttpClient 동작에 문제가 있는 경우, RestTemplate 의 HttpClient 가 변경된 것을 확인해 봐야 한다.
org.apache.httpcomponents:httpclient 는 다른 디펜던시를 통해서도 가져올 수 있으므로, 어플리케이션에서 해당 종속성을 선언하지 않더라도 의존성을 가질 수 있습니다.
Spring-Boot-3.0-Migration-Guide 보기
커스터마이징 된 RestTemplate 예:
fun RestTemplateBuilder.withCustConnectionPool(maxConnTotal:Int): RestTemplateBuilder {
val connectionManager = PoolingHttpClientConnectionManagerBuilder.create()
.setMaxConnTotal(maxConnTotal)
.build()
val client = HttoClientBuilder.create().apply {
// customized
}.setConnectionManager(connectionManager).build()
val rf: Supplier<ClientHttpRequestFactory> = Supplier { HttpComponentsClientHttpRequestFactory(client) }
return this.requestFactory(rf)
}
ConstructingBinding annotations
@ConfigurationProperties 어노테이션은 더이상 @ConstructorBinding 과 함께 사용할 필요가 없고, 여러 생성자를 가지지 않는 경우 클래스에서 제거해야 한다.
이 내용은 다른 Spring boot3 migration guide 에서 언급되어 있다.
OpenAPI generator
Spring Boot 3.x 에서 OpenAPI 생성기를 사용하려면 다음의 설정을 추가해줘야 한다.
"useSpringBoot3" to "true",
"useJakartaEe" to "true"
다음의 가이드를 참조:
Documentation for the spring Generator | OpenAPI Generator
METADATA
openapi-generator.tech
연관된 디펜던시는 다음과 같다
implementation("org.yaml:snakeyaml:2.0")
implementation("io.swagger.parser.v3.swagger-parser:2.1.15")
implementation("org.openapitools:openapi-generator-gradle-pluin-api:6.5.0")
Spring Security
Spring Boot3 에서는 WebSecurityConfigurerAdapter 가 Deprecated 되었다.
from
@Configuration
open class SecurityConfiguration: WebSecurityConfigurerAdapter() {
@Override
override fun configure(val http: HttpSecurity) {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
httpBasic {}
}
}
}
to
@Configuration
open class SecurityConfiguration {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
httpBasic {}
}
return http.build()
}
}
또한, In-Memory Authentication 도 다음과 같이 변경이 필요하다
from
@Configuration
open class SecurityConfiguration: WebSecurityConfigurerAdapter() {
override fun configure(auth: AuthenticationManagerBuilder auth) {
auth.inMemoryAuthentication()
.withUser("username")
.password("{noop}pass")
.authorities(listOf())
}
}
to
@Configuration
open class SecurityConfiguration {
@Bean
fun userDetailService():InmemoryUserDetailsManager {
val user: UserDetails = User.withDefaultPasswordEncoder()
.username("username")
.password("pass")
.authorities(listOf())
.build()
return InMemoryUserDetailsManager(user)
}
}
참고: encryptor 를 나타내는 {noop} 를 사용할 수 없음.
더 자세한 정보는 다음의 가이드를 참조:
Configuration Migrations :: Spring Security
In 6.0, @Configuration is removed from @EnableWebSecurity, @EnableMethodSecurity, @EnableGlobalMethodSecurity, and @EnableGlobalAuthentication. To prepare for this, wherever you are using one of these annotations, you may need to add @Configuration. For ex
docs.spring.io
Spring Security without the WebSecurityConfigurerAdapter
In Spring Security 5.7.0-M2 we deprecated the WebSecurityConfigurerAdapter, as we encourage users to move towards a component-based security configuration. To assist with the transition to this new style of configuration, we have compiled a list of common
spring.io
Spring-Kafka
Transaction-Id
각 프로듀서의 transaction.id 속성은 transactionIdPrefix + n 이며, n 은 0부터 시작하고 새로운 프로듀서마다 증가한다. 이전 버전의 Spring for Apache Kafka 에서는 레코드 기반의 리스너를 가진 컨테이너에 의해 시작된 트랜잭션의 지원을 위한 transaction.id 가 다르게 생성되었다.
3.0 버전부터는 EOSMode.V2 가 유일한 옵션이 되어 더 이상 이러한 이러한 방식의 처리가 필요없게 되었다. 여러 인스턴스로 실행되는 어플리케이션의 경우, transactionIdPrefix 가 인스턴스마다 고유한 값을 가져야 한다.
transaction-id-prefix 디자인은 3.0 버전부터 변경되었고, 다음의 커밋을 참조하세요.
Remove outdated information for transactional.id (#2524) · spring-projects/spring-kafka@7e336d1
* Remove outdated information for transactional.id Compare https://github.com/spring-projects/spring-kafka/issues/2515 With only EOSMode v2 supported, the alternative way of `transactional.id...
github.com
Spring-Kafka 3 에서는 각 어플리케이션 인스턴스에 고유한 Transaction-Id-Prefix 를 할당해야 한다. 일반적으로 아래와 같이 무작위 문자열 또는 호스트 이름을 사용하면 된다.
fun uniqueTransactionIdPrefix(producerOnly: String = "TX-") {
return InetAddress.getLocalHost().getHostName() + this.transactionIdPrefix + producerOnly
}
참고: producerOnly 는 consumer-initiated producer와 producer-only 를 구분하기 위해 사용된다.
쿠버네티스 환경에서는 호스트 이름이 재시작 후 변결될 수 있기 때문에 이 방법이 이상적인 해결책은 아니다. 하지만, 재시작 시간이 transaction.timeout.ms 보다 길다면 문제가 없을 것이다.
더 자세한 정보를 확인하려면 다음의 스택오버플로우를 참조하세요.
transaction-id-prefix need to be identical for same application instance in spring kafka 3?
I understand in spring kafka 3, I need a unique transaction-id-prefix for applications running with multiple instances. here is my configuration: fun getTxnIdPrefix() { return "$hostname-
stackoverflow.com
More about Kafka transaction
다음의 아티클은 Kafka 트랜잭션과 멱등성에 대해 설명하고 있다.
Transactions in Apache Kafka | Confluent
Learn the main concepts needed to use the transaction API in Apache Kafka effectively.
www.confluent.io
이 링크는 spring-kafka 2.x 에서 transaction-id-prefix 를 구성하는 방법에 대핸 정보를 제공합니다. Kafka 프로듀서의 디자인을 이해하는데 Spring boot3 에서도 도움이 됩니다.
기본적으로 Kafka 프로듀서는 transaction-id-prefix-atomic_inc_id 와 같이 고유한 ID 를 가지며, 데이터를 보내기 위한 KafkaTemplate을 사용하려고 할때는 캐시에서 사용 가능한 프로듀서를 가져와 트랜잭션 커밋 후 반환합니다. 또한, 트랜잭션이 커밋된 후에 프로듀서 에포크를 업데이트 합니다.
자주 관찰되는 문제 중 하나는 ProducerFencedException 입니다. 이것은 프로듀서가 더 높은 epoch 를 가지고 있지만 동일한 프로듀서ID 를 가진 다른 프로듀서(혹은 좀비) 가 여전히 더 낮은 epoch 로 커밋하려고 시도하여 프로듀서가 차단된 상황을 의미합니다. 이 문제는 클라이언트 리밸런스 또는 프로그래밍 버그로 인하여 발생합니다.
다음의 스택오버플로우에서 더 많은 정보를 확인할 수 있다.
Why my Spring Kafka unit test almost ran into ProducerFencedException every time
The test is consist of 2 test cases @Test @Nested inner class Test1 { fun test1() { mypublisher.publishInTransaction(topic1) // see log1 // check listener1 // see log2 } } @Test @
stackoverflow.com
KafkaTemplate 변경
3.0 버전에서는 이전에 ListenableFuture 를 반환하는 메소드가 CompletableFuture 를 반환하도록 변경되었습니다. 이전 버전인 2.9 에서는 CompletableFuture 반환 유형을 가진 동일한 메소드를 제공하는 usingCompletableFuture() 가 추가되었지만, 이 메소드는 더 이상 사용할 수 없습니다.
from:
kafkaTemplate.send(producerRecord).addCallback(
{
result: SendResult<Any?, Any?>? ->
log.info("xxxx")
},
{
ex: Throwable? ->
log.error("xxx")
}
)
to:
kafkaTemplate.send(producerRecord).whenComplete { result, ex ->
if (ex!=null) {
// exception
} else {
// successfully sent
}
}
JacksonObjectMapper
문제점
No Serializer found for class org.springframework.http.HttpMethod and no properties discovered to create BeanSerializer
Spring Boot 3 에서는 HttpMethod 멤버변수가 private 으로 변경되어 ObjectMapper 가 private 변수를 직렬화 할 수 없습니다.
해결방안
Serializer 를 재정의하거나 jacksonObjectMapper 가 private 속성을 직렬화 할 수 있도록 허용해줘야 합니다.
jacksonObjectMapper().setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
'Medium' 카테고리의 다른 글
| Spring Boot3 with QueryDSL - Part1 (0) | 2023.11.03 |
|---|---|
| Retry 와 Fallback 메카니즘을 활용한 스프링 마이크로서비스 회복탄력성 (0) | 2023.10.26 |
| 소프트웨어 엔지니어가 알아야 할 12가지 소프트웨어 아키텍처 스타일 (0) | 2023.10.23 |
| 스프링 마이크로서비스와 사이드카 패턴 (0) | 2023.10.19 |
| Java Records: 언제 그리고 왜 사용해야 하나? (0) | 2023.10.16 |