⚡ 개요
Spring boot 2.6.x 버전이 2024년 2월달 부로 지원이 종료가 된다. :(
아직 까지는 크게 문제가 없지만 결국은 버전을 올려야 하며, 내부 버전을 마이그레이션을 하기로 생각했다.
아마 추가적으로 다른 내용들도 정리를 하고 있지만 가장 먼저 QueryDsl 의 Dialect의 사용 방식이 변경돼서 이 부분에 대해서 정리를 하려고 한다.
공식 문서 및 관련 자료를 찾아도 방법에 대해서 공유를 해놓은 사람이 없어서 내부 클래스를 분석해서 사용 방법을 재정의 했다. 이에 대한 내용을 정리해보려고 한다.
📚 버전 정보
현재 테스트를 할떄 필요한 버전 정보만 공유하도록 하겠다.
- Spring boot 2.6.x ==> Spring boot 2.7.18 ==> Spring boot 3.0.18
- Spring boot의 경우는 마이그레이션 할때 순차적으로 마이그레이션을 하는 게 좋다고 하여, 순차적으로 마이그레이션을 진행했으며 2.7.x 버전에서는 크게 변경된 부분이 없어서 문제가 발생하지 않았다.
- 추가적으로 javax -> jakarta 로 변경되었는데 intelij에서는 마이그레이션을 도와주니까 너무 겁먹지 말고 진행해도 된다. (참조)
- Hibernate 5.6.15.Final ==> Hibernate 6.1.7.Final
- spring boot 3.x.x 버전으로 올라오면서 종속된 라이브러리들의 버전이 업데이트 되었다.
그중에서 지금 QueryDsl에 직접적으로 영향이 가는 라이브러리라서 명시적으로 적어보았다. 내부적으로 삭제된 클래스들이 있고 새로 정의가 되고 좀 더 명확히 바뀐 부분도 존재한다.
- spring boot 3.x.x 버전으로 올라오면서 종속된 라이브러리들의 버전이 업데이트 되었다.
⚡ Spring boot 3.x.x 에서 QueryDsl Date Type 사용 방법
버전이 업데이트된 상태에서 QueryDsl Date Type을 사용하는 방법에 대해서 정리하려고 한다.
일단 기존 Hibernate 6.x.x 버전 밑에서 사용하는 방법에 대해서 정리를 하면서 6.x.x 버전에서 사용하는 방법까지 같이 정리를 하도록 하겠다.
📗 Dialect
먼저 Dialect (방언) 에 대해서 다시 한번 정리를 하자면 DBMS 별로 문법이 다르며 사용하는 함수가 다르다.
JPA는 정해진 Dialect에 따라서 문법을 해석하고 내부적으로 쿼리를 날려주게 된다.
우리는 JPA를 사용하고 방언 설정만 하게 되면 알아서 SQL을 작성하고 내가 원하는 쿼리를 날려주게 된다.
# 각 DB 별로 방언에 대한 클래스를 제공하기 때문에 어떤 방언을 사용할지 정의 하고 사용하면 된다.
📘 버전별 사용 방법 정리
우리는 내부적으로 MariaDBDialect 을 사용하고 있으며, 이를 베이스로 정리를 하려고 한다. 물론 다른 Dialect을 사용한다고 내부 로직이 바뀌는 것은 아니기 때문에 크게 상관은 없고 추가적인 설명은 소스를 보면서 설명을 하려고 한다.
버전이 업데이트돼도 당연히 외부 함수는 사용할 수가 없고 정의를 해서 사용을 해야 한다. Dialect 에서는 내장 함수만 지원을 한다는 얘기이다. 내부적으로 버전이 업데이트 되면서 Dialect 클래스 내부에 함수가 추가돼서 사용을 할 수 있을 수도 있지만 없을 수도 있다는 얘기이며, 이 부분은 추가적으로 Dialect 내부 클래스를 뜯어서 확인해보는 것을 추천한다.
본문으로 들어가서 우리는 지난 버전에서 DateType을 변환하는 부분에 대해서 MariaDBDialect에 내장 함수로 존재가 하지 않아서 이 부분을 커스텀해서 사용자 정의 함수를 생성해서 사용을 했고 버전이 업데이트되면서 사용 방식이 변경이 되었다.
✏️ Hibernate 5.x.x 버전 방식
public class MariaDbDialectCustom extends MariaDBDialect {
public MariaDbDialectCustom() {
super();
// EX) DATETIME_TO_LOCALTIME({0}, '%T')
this.registerFunction("DATETIME_TO_LOCALTIME", new StandardSQLFunction("str_to_date", new LocalTimeType()));
// EX) DATETIME_TO_LOCALTIME({0}, '%Y-%m-%d')
this.registerFunction("DATETIME_TO_LOCALDATE", new StandardSQLFunction("str_to_date", new LocalDateType()));
// EX) DATETIME_TO_LOCALDATETIME({0}, '%Y-%m-%d %T')
this.registerFunction("DATETIME_TO_LOCALDATETIME", new StandardSQLFunction("str_to_date", new LocalDateTimeType()));
}
}
일단 먼저 MariaDBDialect를
위의 이미지처럼 사용자 정의로 사용할 명칭과 사용할 함수 및 리턴값을 정의한 SQLFunction을 생성하면 QueryDsl에서 해당 정의된 함수명칭으로 사용이 가능했다.
여기서 지정한 명칭을 기준으로 static으로 호출 함수를 하나 만들고 사용하면 내부적으로 사용 방식이 바뀌어도 해당 부분만 변경하게 되면 다른 곳에서 사용할 때 크게 문제가 없다.
✏️ Hibernate 6.x.x 버전 방식
업데이트된 Hibernate 6.x.x 버전부터는 this.registerFunction() 함수가 없어졌으며 사용자 지정 함수를 사용하지 못하도록 되었다. 이 부분에 대해서 각종 공식 문서 및 stackoverflow, google 등을 찾아봤으나 어떻게 해야지 내가 원하는 값이 나오는지 찾을 수 없어서 내부 구현체들을 찾아보고 분석을 하게 되었다.
상속을 받은 Dialect 클래스 내부를 살펴보면 initializeFunctionRegistry(QueryEngine queryEngine) 클래스가 존재한다.
여기서 함수들을 지정하고 사용하고 있는 것으로 파악을 했고, 공식 문서와 비교해서 테스트 코드를 작성하고 테스트했다.
// 초기 샘플 소스
public void initializeFunctionRegistry(QueryEngine queryEngine) {
super.initializeFunctionRegistry(queryEngine);
queryEngine.getSqmFunctionRegistry().registerPattern("DATETIME_TO_LOCALTIME", "str_to_date({0}, '%T')", queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve(StandardBasicTypes.LOCAL_TIME));
queryEngine.getSqmFunctionRegistry().registerPattern("DATETIME_TO_LOCALDATE", "str_to_date({0}, '%Y-%m-%d')", queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve(StandardBasicTypes.LOCAL_DATE));
queryEngine.getSqmFunctionRegistry().registerPattern("DATETIME_TO_LOCALDATETIME", "str_to_date({0}, '%Y-%m-%d %T')", queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve(StandardBasicTypes.LOCAL_DATE_TIME));
}
이전에 사용했던 MariaDbDialectCustom 클래스에서 Dialect 클래스에 존재하는 initializeFunctionRegistry() 함수를 오버라이딩 해서 내가 필요한 함수를 재정의 해줬다.
클래스를 재정의 하고 쿼리를 호출하면 위와 같은 에러가 발생하게 된다.
뭔가 사용하는 방법이 맞는 거 같으면서도 아닌 거 같고, 추가적으로 설정만 몇 개 더 추가하면 될 거 같은 느낌이 들었고 내부 클래스를 좀 더 분석하고 테스트해보았고 결론적으로 아래와 같은 아주 기본적인 소스를 만들었다.
// 샘플 소스
public void initializeFunctionRegistry(QueryEngine queryEngine) {
super.initializeFunctionRegistry(queryEngine);
queryEngine.getSqmFunctionRegistry().patternDescriptorBuilder("DATETIME_TO_LOCALTIME", "str_to_date(?1, '%T')")
.setExactArgumentCount(2)
.setParameterTypes(FunctionParameterType.TIME, FunctionParameterType.STRING)
.setInvariantType(queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve(StandardBasicTypes.LOCAL_TIME))
.register();
queryEngine.getSqmFunctionRegistry().patternDescriptorBuilder("DATETIME_TO_LOCALDATE", "str_to_date(?1, '%Y-%m-%d')")
.setExactArgumentCount(2)
.setParameterTypes(FunctionParameterType.TIME, FunctionParameterType.STRING)
.setInvariantType(queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve(StandardBasicTypes.LOCAL_DATE))
.register();
queryEngine.getSqmFunctionRegistry().patternDescriptorBuilder("DATETIME_TO_LOCALDATETIME", "str_to_date(?1, '%Y-%m-%d %T')")
.setExactArgumentCount(2)
.setParameterTypes(FunctionParameterType.TIME, FunctionParameterType.STRING)
.setInvariantType(queryEngine.getTypeConfiguration().getBasicTypeRegistry().resolve(StandardBasicTypes.LOCAL_DATE_TIME))
.register();
}
소스를 보면 알겠지만 argument와 parameterType을 지정해 주고 리턴 받을 타입까지 지정한 뒤에 레지스트리를 호출하면 된다. 방언 클래스를 분석해서 내장 함수들을 지정하는 방식과 유사하게 만들어 보았고 정상적으로 Date Type을 반환해서 기존 로직이 정상적으로 동작하도록 만들었다.
위의 코드는 내장 함수를 기반으로 만들었기 때문에 충분히 더 간략하고 명시적인 코드로 만들 수 있는 것은 사실이다.
심지어 내장 함수에는 이미 지원하는 함수도 존재한다. 하지만 우리 코드 상에서 이미 위에서 지정한 명칭으로 쿼리를 호출해서 사용을 하고 있기 때문에 전체를 바꾸는 것보다는 재정의를 해서 사용하는 것이 맞다고 판단을 하였고 위와 같이 수정을 하게 되었다.
지금은 내장 함수를 기반으로 좀 간략한 코드가 나왔지만 위의 코드가 베이스라고 생각을 해서 정리를 하게 되었다. 위의 코드를 기준으로 변환을 해서 사용한다면 지금은 Date Type 만 샘플 코드로 작성했지만 다른 코드도 얼마든지 만들 수 있다고 생각했다.
⚡생각
이미 내부적으로 Spring boot 3.x.x 버전으로 마이그레이션 1차가 진행되었다. 새로운 브랜치를 따고 라이브러리 버전을 올려가면서 충돌된 내용을 분석하고 더 좋은 방향성으로 수정하기 위해서 다른 마이그레이션 소스도 참조를 하고 있다.
위의 내용뿐만 아니라 다른 부분에서도 사용 방식이 바뀌면서 다양한 이슈들이 발견되고 수정하고 있다.
예를 들어 log 관련 설정도 properties에서 변경이 되었으니 한번 찾아보면 좋을 거 같다.
'Spring' 카테고리의 다른 글
Spring boot + vue 프로젝트 (2) (0) | 2024.04.06 |
---|---|
Spring boot + vue 프로젝트 (1) (0) | 2024.04.04 |
[Spring] Spring boot 3.x.x 버전 마이그레이션 내용 정리 (0) | 2023.12.31 |
[JPA] save() 와 saveAndFlush() 차이점 정리 (0) | 2023.10.29 |
[JPA] repository를 통해서 일부 컬럼만 조회하는 방법 정리 (0) | 2023.10.09 |