3 분 소요

JDK 버전만 업그레이드하는건 어렵지 않지만, 프로젝트에서 같이 사용하는 라이브러리, 프레임워크 등의 버전을 올리고 경우에 따라서는
코드도 수정해주는 작업이 필요하다. 대표적인 예로 기존에 쓰던 기능이 제거되었거나 deprecated 된 케이스가 다수 있다.

이 글에서는 주로 그런 케이스들을 정리했다. Spring Boot도 2.x -> 3.x로 올렸기에 비슷한 스택인 프로젝트에서 JDK 버전업을
계획중이라면 조금이나마 도움되길 바라는 마음으로 썼다.

패키지 이름, 경로 변경

javax -> jakarta

가장 컴파일이 많이 깨지는 케이스로 패키지명이 javax.*에서 jakarta.*로 변경되었다. (JDK 17+)
참고로 javax.crypto는 jakarta로 안바뀌었다. (crypto는 Java EE 소속이 아니어서 여전히 javax라고 함)

apache lang -> lang3

과거 org.apache.commons.lang.*에서 org.apache.commons.lang3.*로 변경되었다. (의존성 변경도 필요함)
lang도 JDK 21과 호환은 된다. 다만 보안 취약점(CVE-2025-48924) 개선이 되어서 lang3으로 같이 올리는게 좋을 것 같다.

PageableExecutionUtils 패키지 변경

  • as-is: org.springframework.data.repository.support.PageableExecutionUtils
  • to-be: org.springframework.data.support.PageableExecutionUtils

repository가 중간에 빠졌다. 컴파일 단계에서 잡아낼 수 있다.

yml 설정

feign -> spring.cloud.openfeign

순수 Feign(Netflix) 라이브러리 자체 설정과 Spring Cloud가 래핑한 OpenFeign의 설정을 구분하기 위해 설정이 바뀌었다.
아래처럼 수정하면 된다.

# as-is
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full

# to-be
spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 5000
            loggerLevel: full

spring.profiles -> spring.config.activate.on-profile

Spring Boot 2.4부터 반영된 내용이긴 하지만 3.x로 올리면 spring.profiles를 아예 사용할 수 없게된다.
spring.config.activate.on-profile로 바꿔주자.

# as-is
spring:
  profiles: dev

---

# to-be
spring:
  config:
    activate:
      on-profile: dev

안바꿔주면 아래 에러가 발생한다. 말 그대로 바꿔주면 된다.

org.springframework.boot.context.config.InvalidConfigDataPropertyException:
Property 'spring.profiles' imported from location '...' is invalid and should be replaced with 'spring.config.activate.on-profile' 

코드 수정

@Retryable 필드: value (deprecated) -> retryFor

value가 deprecated 되고 retryFor가 대체하게 되었다.

// as-is
@Retryable(value = {RuntimeException.class})

// to-be
@Retryable(retryFor = {RuntimeException.class})

hibernate naming strategy 변경: SpringPhysicalNamingStrategy -> CamelCaseToUnderscoresNamingStrategy

Unable to resolve name [org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy] as strategy

Spring Boot 3.x로 올리면서 hibernate도 6버전으로 올리게 되었는데 SpringPhysicalNamingStrategy가 제거됐다.
CamelCaseToUnderscoresNamingStrategy로 바꿔서 사용하면 된다.

org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
-> org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy
  • 패키지명을 보니 strategy를 springframework가 아니라 hibernate에서 관리하려는 의도로 보인다

PostgreSQL10Dialect 제거

hibernate 버전업이 되면서 더 이상 PostgreSQL10Dialect를 관리하지 않아서 방언을 찾지못해 bootRun 과정에서 실패하는걸 확인할 수 있었다. hibernate.dialect에 명시적으로 설정을 추가해줬었다면 빼버리자. 알아서 PostgreSQLDialect가 등록된다. (stackoverflow 참고)

BeanDefinitionRegistryPostProcessor Bean 초기화

BeanDefinitionRegistryPostProcessor 계열 클래스를 Bean으로 등록하는 경우, BeanDefinitionRegistryPostProcessor는 post-processor이기 때문에 이른 시점에 초기화돼야 한다. 근데 그러려면 해당 빈 메서드가 정의된 Configuration 클래스 또한 이른 시점에 인스턴스화된다. 그니까 Configuration이 CGLIB에 의해 처리되기 전에 인스턴스화 돼서 proxy로서 동작을 못한다.

이렇게되면 BeanDefinitionRegistryPostProcessor는 Bean으로 등록이 잘 되었어도 Configuration은 proxy 역할을 못하므로 self-invocation 케이스에서 빈을 사용하지 못한다. Configuration이 proxy가 아니기 때문이다.

@Configuration
public class MyConfig { // 2. PostProcessor와 같이 인스턴스화. proxy 역할이 안됨.

    @Bean
    public A a() {
        return new A();
    }

    @Bean
    public B b() {
        return new B(a()); // 3. a()는 bean이 아니고 새 인스턴스가 들어감. (MyConfig가 proxy 역할을 못하기 때문)
    }

    @Bean
    public MyRegistryPostProcessor myRegistryPostProcessor() { // 1. PostProcessor가 있어서 CGLIB enhancement 전에 인스턴스화
        return new MyRegistryPostProcessor();
    }
}

public class MyRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { ... }

BeanDefinitionRegistryPostProcessor beans should be declared via static @Bean methods to avoid early instantiation issues.

이럴땐 PostProcessor Bean 메서드에 static을 붙여주라고 가이드해주고 있다.
이러면 MyConfig(Configuration)이 조기에 인스턴스화되지 않아도 된다.
(MyRegistryPostProcessor Bean 등록 시 MyConfig.myRegistryPostProcessor()를 호출하면 되니까)

@Bean
public static MyRegistryPostProcessor myRegistryPostProcessor() {
    return new MyRegistryPostProcessor();
}
  • 그러면 MyConfig도 CGLIB enhancement 단계에서 정상적으로 proxy로 생성된다

ObjectMapper는 주입 받아서 사용하기

InvalidDefinitionException: Java 8 date/time type java.time.LocalDateTime not supported by default: add Module “com.fasterxml.jackson.datatype:jackson-datatype-jsr310” to enable handling

new ObjectMapper()로 직접 인스턴스화해서 사용하는 경우 LocalDateTime을 직렬화하지 못하는 이슈가 발생했다.
핸들링할 모듈이 없다고 말해주고 있고, ObjectMapper를 Spring Boot로부터 주입받아 사용하면 해결된다.
(JavaTimeModule를 등록해둔 ObjectMapper Bean이 기본으로 관리되고 있다)

아니면 빈을 Jackson2ObjectMapperBuilder를 통해 생성하거나, 직접 JavaTimeModule를 등록해서 사용하면 된다.

// Jackson2ObjectMapperBuilder 활용
@Bean
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
    return builder.build();
}

// JavaTimeModule 직접 등록
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());

KafkaTemplate.send(): ListenableFuture -> CompletableFuture

ListenableFuture가 deprecated 되어서 CompletableFuture로 대체되었다.
KafkaTemplate.send() 반환 타입 및 callback 로직도 고쳐주자.

// as-is
ListenableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, message);

future.addCallback(new ListenableFutureCallback<>() {
    @Override
    public void onSuccess(SendResult<String, String> result) {
        System.out.println("Sent: " + result.getRecordMetadata().offset());
    }

    @Override
    public void onFailure(Throwable ex) {
        System.err.println("Failed: " + ex.getMessage());
    }
});

// to-be
CompletableFuture<SendResult<String, String>> future = kafkaTemplate.send(topic, message);

future.whenComplete((result, ex) -> {
    if (ex == null) {
        System.out.println("Sent: " + result.getRecordMetadata().offset());
    } else {
        System.err.println("Failed: " + ex.getMessage());
    }
});

References

카테고리:

업데이트:

댓글남기기