Querydsl vs. JPA Criteria, 제6부: Spring Data JPA 및 Querydsl 프로젝트를 위한 Spring Boot 3.2 업그레이드 가이드

작년에 저는 Spring Boot 3.0.x 업그레이드를 위한 “Spring Data JPA 및 Querydsl을 위한 Spring Boot 3.0 업그레이드 가이드“라는 글을 썼습니다. 이제 우리는 Spring Boot 3.2를 가지고 있습니다. Spring Boot 3.2.2로 업그레이드할 때 다룰 수 있는 두 가지 문제를 살펴보겠습니다.

SAT 프로젝트에서 사용되는 기술은 다음과 같습니다:

  1. Spring Boot 3.2.2 및 Spring Framework 6.1.3
  2. Hibernate + JPA 모델 생성기 6.4.1. Final
  3. Spring Data JPA 3.2.2
  4. 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 빌드)를 컴파일하면 다음과 같은 오류로 실패합니다:

Plain Text

 

[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-jpamodelgenquerydsl-apt (이 경우)를 제거할 수 있습니다. 대신, pom.xml에서 maven-compiler-plugin을 통해 정적 메타모델 생성기를 이렇게 정의해야 합니다:

XML

 

<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을 이러한 방식으로 사용하지 않으면 다음과 같은 컴파일 오류가 발생합니다:

Plain Text

 

[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에도 적용됩니다. 이 경우 다음과 같은 컴파일 오류를 볼 수 있습니다:

Plain Text

 

[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의 직렬화가 Unpagedenum에서 class로 변경하면서 영향을 받았습니다 (참조 spring-projects/spring-data-commons#2987).

  • 스프링 부트 3.1.6:
Java

 

public interface Pageable {

	static Pageable unpaged() {
		return Unpaged.INSTANCE;
	}

	...
}

enum Unpaged implements Pageable {

	INSTANCE;

	...
}

  • 스프링 부트 3.2.2:
Java

 

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)를 사용할 때 (우리가 평소에 사용하던 방식) 다음과 같은 오류가 발생합니다:

Plain Text

 

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

해결책은 모든 속성을 사용하는 생성자를 사용하는 것입니다:

Java

 

new PageImpl<City>(cities, ofSize(PAGE_SIZE), cities.size())

대신:

Java

 

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