작년에 저는 Spring Boot 3.0.x 업그레이드를 위한 “Spring Data JPA 및 Querydsl을 위한 Spring Boot 3.0 업그레이드 가이드“라는 글을 썼습니다. 이제 우리는 Spring Boot 3.2를 가지고 있습니다. Spring Boot 3.2.2로 업그레이드할 때 다룰 수 있는 두 가지 문제를 살펴보겠습니다.
SAT 프로젝트에서 사용되는 기술은 다음과 같습니다:
- Spring Boot 3.2.2 및 Spring Framework 6.1.3
- Hibernate + JPA 모델 생성기 6.4.1. Final
- Spring Data JPA 3.2.2
- Querydsl 5.0.0.
변경사항
Spring Boot 3.2의 모든 변경사항은 Spring Boot 3.2 릴리스 노트 및 버전 6.1의 새로운 기능에 대한 Spring Framework 6.1에 설명되어 있습니다.
Spring Boot 3.2.2의 최신 변경사항은 GitHub에서 확인할 수 있습니다.
발견된 문제
- A different treatment of Hibernate dependencies due to the changed
hibernate-jpamodelgen
behavior for annotation processors Unpaged
클래스가 재설계되었습니다.
우선 Hibernate 의존성부터 시작합시다.
정적 메타모델 생성 통합
가장 큰 변화는 정적 메타모델을 생성하는 hibernate-jpamodelgen
의존성에서 발생합니다. Hibernate 6.3에서는 전이적 의존성을 완화하기 위해 의존성 처리가 변경되었습니다. Spring Boot 3.2.0은 hibernate-jpamodelgen
의존성을 6.3 버전으로 업그레이드했습니다(참조 의존성 업그레이드). 불행히도 새 버전은 컴파일 오류를 일으킵니다(아래 참조).
참고: 여기서 사용된 Spring Boot 3.2.2는 이미 동일한 동작을 가진 Hibernate 6.4를 사용합니다.
컴파일 오류
이 변경으로 인해 Spring Boot 3.2.2를 사용하여 프로젝트(Maven 빌드)를 컴파일하면 다음과 같은 오류로 실패합니다:
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.049 s
[INFO] Finished at: 2024-01-05T08:43:10+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project sat-jpa: Compilation failure: Compilation failure:
[ERROR] on the class path. A future release of javac may disable annotation processing
[ERROR] unless at least one processor is specified by name (-processor), or a search
[ERROR] path is specified (--processor-path, --processor-module-path), or annotation
[ERROR] processing is enabled explicitly (-proc:only, -proc:full).
[ERROR] Use -Xlint:-options to suppress this message.
[ERROR] Use -proc:none to disable annotation processing.
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityRepository.java:[3,41] error: cannot find symbol
[ERROR] symbol: class City_
[ERROR] location: package com.github.aha.sat.jpa.city
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityRepository.java:[3] error: static import only from classes and interfaces
...
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[4] error: static import only from classes and interfaces
[ERROR] java.lang.NoClassDefFoundError: net/bytebuddy/matcher/ElementMatcher
[ERROR] at org.hibernate.jpamodelgen.validation.ProcessorSessionFactory.<clinit>(ProcessorSessionFactory.java:69)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMeta.handleNamedQuery(AnnotationMeta.java:104)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMeta.handleNamedQueryRepeatableAnnotation(AnnotationMeta.java:78)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMeta.checkNamedQueries(AnnotationMeta.java:57)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMetaEntity.init(AnnotationMetaEntity.java:297)
[ERROR] at org.hibernate.jpamodelgen.annotation.AnnotationMetaEntity.create(AnnotationMetaEntity.java:135)
[ERROR] at org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor.handleRootElementAnnotationMirrors(JPAMetaModelEntityProcessor.java:360)
[ERROR] at org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor.processClasses(JPAMetaModelEntityProcessor.java:203)
[ERROR] at org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor.process(JPAMetaModelEntityProcessor.java:174)
[ERROR] at jdk.compiler/com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:1021)
[ER...
[ERROR] at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:348)
[ERROR] Caused by: java.lang.ClassNotFoundException: net.bytebuddy.matcher.ElementMatcher
[ERROR] at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:445)
[ERROR] at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:593)
[ERROR] at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
[ERROR] ... 51 more
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException
이것은 Hibernate 마이그레이션 가이드에서 발표된 정적 메타모델 생성 방식의 변경 때문입니다(참조 정적 메타모델 생성 통합 및 원래 이슈 HHH-17362). 이러한 변경에 대한 그들의 설명은 다음과 같습니다:
“… 이전 버전의 Hibernate ORM에서는
hibernate-jpamodelgen
의 의존성을 무의식적으로 컴파일 클래스패스에 누수시켰습니다. Hibernate ORM 6.3에서는 Antlr 클래스와 관련하여 어노테이션 처리 중 컴파일 오류가 발생할 수 있습니다.”
의존성 변경
아래 스크린샷에서 볼 수 있듯이 Hibernate 의존성이 실제로 변경되었습니다.
- 스프링 부트 3.1.6:
- 스프링 부트 3.2.2:
설명
마이그레이션 가이드에서 언급한 바와 같이, 간단한 Maven 의존성에서 Maven 컴파일러 플러그인의 어노테이션 처리기 경로로 pom.xml
을 변경해야 합니다 (참조: 문서).
해결책
마지막 문서에서 권장하듯이 Maven 의존성인 hibernate-jpamodelgen
과 querydsl-apt
(이 경우)를 제거할 수 있습니다. 대신, pom.xml
에서 maven-compiler-plugin
을 통해 정적 메타모델 생성기를 이렇게 정의해야 합니다:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-jpamodelgen</artifactId>
<version>${hibernate.version}</version>
</path>
<path>
<groupId>com.querydsl</groupId>
<artifactId>querydsl-apt</artifactId>
<version>${querydsl.version}</version>
<classifier>jakarta</classifier>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
GitHub의 SAT 프로젝트에서 관련 변경 사항을 참조하세요.
하이버네이트-jpamodelgen 때문에 이러한 방식을 사용해야 하는 경우, 어노테이션 처리와 밀접한 모든 의존성들에 이를 적용해야 합니다 (querydsl-apt
또는 lombok
등). 예를 들어, lombok
을 이러한 방식으로 사용하지 않으면 다음과 같은 컴파일 오류가 발생합니다:
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR :
[INFO] -------------------------------------------------------------
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityService.java:[15,30] error: variable repository not initialized in the default constructor
[INFO] 1 error
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 4.535 s
[INFO] Finished at: 2024-01-08T08:40:29+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project sat-jpa: Compilation failure
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\city\CityService.java:[15,30] error: variable repository not initialized in the default constructor
마찬가지로 querydsl-apt
에도 적용됩니다. 이 경우 다음과 같은 컴파일 오류를 볼 수 있습니다:
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.211 s
[INFO] Finished at: 2024-01-11T08:39:18+01:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.11.0:compile (default-compile) on project sat-jpa: Compilation failure: Compilation failure:
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryRepository.java:[3,44] error: cannot find symbol
[ERROR] symbol: class QCountry
[ERROR] location: package com.github.aha.sat.jpa.country
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryRepository.java:[3] error: static import only from classes and interfaces
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[3,41] error: cannot find symbol
[ERROR] symbol: class QCity
[ERROR] location: package com.github.aha.sat.jpa.city
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[3] error: static import only from classes and interfaces
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[4,44] error: cannot find symbol
[ERROR] symbol: class QCountry
[ERROR] location: package com.github.aha.sat.jpa.country
[ERROR] <SAT_PATH>\sat-jpa\src\main\java\com\github\aha\sat\jpa\country\CountryCustomRepositoryImpl.java:[4] error: static import only from classes and interfaces
[ERROR] -> [Help 1]
이유는 명확합니다. 모든 어노테이션 프로세서를 동시에 적용해야 합니다. 그렇지 않으면 일부 코드가 누락되어 컴파일 오류가 발생합니다.
Unpaged 재설계
두 번째 사소한 문제는 Unpaged
클래스의 변경과 관련이 있습니다. 잭슨 라이브러리에 의한 PageImpl
의 직렬화가 Unpaged
를 enum
에서 class
로 변경하면서 영향을 받았습니다 (참조 spring-projects/spring-data-commons#2987).
- 스프링 부트 3.1.6:
public interface Pageable {
static Pageable unpaged() {
return Unpaged.INSTANCE;
}
...
}
enum Unpaged implements Pageable {
INSTANCE;
...
}
- 스프링 부트 3.2.2:
public interface Pageable {
static Pageable unpaged() {
return unpaged(Sort.unsorted());
}
static Pageable unpaged(Sort sort) {
return Unpaged.sorted(sort);
}
...
}
final class Unpaged implements Pageable {
private static final Pageable UNSORTED = new Unpaged(Sort.unsorted());
...
}
new PageImpl<City>(cities)
를 사용할 때 (우리가 평소에 사용하던 방식) 다음과 같은 오류가 발생합니다:
2024-01-11T08:47:56.446+01:00 WARN 5168 --- [sat-elk] [ main] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: (was java.lang.UnsupportedOperationException)]
MockHttpServletRequest:
HTTP Method = GET
Request URI = /api/cities/country/Spain
Parameters = {}
Headers = []
Body = null
Session Attrs = {}
Handler:
Type = com.github.aha.sat.elk.city.CityController
Method = com.github.aha.sat.elk.city.CityController#searchByCountry(String, Pageable)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = org.springframework.http.converter.HttpMessageNotWritableException
해결책은 모든 속성을 사용하는 생성자를 사용하는 것입니다:
new PageImpl<City>(cities, ofSize(PAGE_SIZE), cities.size())
대신:
new PageImpl<City>(cities)
참고: 이 문제는 스프링 부트 3.3에서 수정될 예정입니다 (참조 이 이슈 댓글).
결론
이 글에서는 최신 버전인 Spring Boot 3.2.2로 업그레이드하는 과정에서 발견된 문제들을 다룬 바 있습니다(이 글을 작성하는 시점 기준). 글에서는 먼저 변경된 Hibernate 의존성 관리로 인한 어노테이션 프로세서 처리 방식에 대해 설명하였습니다. 다음으로 Unpaged
클래스의 변경과 PageImpl
을 사용하기 위한 해결책에 대해 설명하였습니다.
이러한 모든 변경 사항(그 외 다른 변경 사항 포함)은 PR #64에서 확인할 수 있습니다. 위에서 설명한 전체 소스 코드는 제 GitHub 저장소에서 제공됩니다.
Source:
https://dzone.com/articles/upgrade-guide-to-spring-boot-32-for-spring-data-jp