Medium

Spring Boot3 마이그레이션

ppthejake 2023. 10. 17. 17:26

해당 포스트는 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)

참고 자료

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)