[Hibernate] Migration of 5.4.32.FINAL to 6.5.2.FINAL ISSUE

개요 

Spring Boot 2.5.x에서 Spring Boot 3.3.x로 마이그레이션을 진행 중입니다. 이에 따라 Hibernate 버전도 5.4.32 FINAL에서 6.5.2 FINAL로 업그레이드하였습니다.

이번 마이그레이션 과정에서 몇 가지 이슈가 발생했는데, 이를 해결한 경험을 공유하고자 합니다. 

[트러블 슈팅] SemanticException

SemanticException, java.time.TemporalAmount

문제 상황 : 최근 7일간 접수된 문의 개수를 조회하는 쿼리에서 "java.time.TemporalAmount" 예외가 발생한다.

이전 버전에서 정상적으로 실행되었던 쿼리가 마이그레이션 이후 실행 단계에서부터 예외가 발생했습니다.

Migration of 5.2.17.FINAL to 6.4.1 problems 해당 hibernate 커뮤니티 글에 따르면 ORM 6 버전부터는 엄격한 유효성 검사를 진행한다고합니다. 또한 "정수에 단위가 없기 때문에 날짜에서 정수를 뺄 수 없다"는 내용을 포함하고 있습니다.

 

예외가 발생한 쿼리는 JPQL을 사용했으며, 특정 기간 동안의 데이터를 조회하기 위해 'BETWEEN CURRENT_DATE - 7 AND CURRENT_DATE' 조건을 작성했습니다.

해당 글로 추측했을 때 'CURRENT_DATE - 7' 조건이 ORM 5 버전에서는 허용이 되었지만, ORM 6 버전에서는 허용되지 않는 것으로 추측했습니다. 

 

해결 방안 : nativeQuery 또는 Querydsl을 사용한 쿼리 튜닝을 진행한다.

@Repository
@RequiredArgsConstructor
public class QnaRepositoryImpl implements CustomQnaRepository {

	private final JPAQueryFactory jpaQueryFactory;
    
    // ...

	@Override
	public Long getCountReceivedLast7Days() {
		return jpaQueryFactory
				.select(qna.count())
				.from(qna)
				.where(
						qna.isDeleted.isFalse(),
						qna.status.eq(QnaStatus.RECEIVED),
						qna.createdAt.between(LocalDateTime.now().minusDays(7), LocalDateTime.now())
				)
				.fetchOne();
	}
    
    // ... 생략
    
}

문제를 해결하기 위해 저는 Querydsl을 사용하여 기간 단위를 명시하도록 수정했습니다.

(qna.createdAt.between(LocalDateTime.now().minusDays(7), LocalDateTime.now()))

 

[트러블 슈팅] InvalidDataAccessApiUsageException

InvalidDataAccessApiUsageException, Duplicate identification variable

문제 상황 : 발송 내역을 조회하는 쿼리에서 "InvalidDataAccessApiUsageException" 예외가 발생한다.

이전 버전에서 정상적으로 실행되었던 쿼리가 마이그레이션 이후 API를 호출하는 단계에서 예외가 발생했습니다.

 

AliasCollisionException (Hibernate Javadocs)

AliasCollisionException (Hibernate Javadocs)에 따르면, AliasCollisionException 예외는 FROM 절에 동일한 식별 변수를 중복 선언하거나 SELECT 절에서 동일한 결과 열 별칭을 중복 선언할 때 발생한다고 합니다.

 

.from(push)
.innerJoin(push.createdBy, admin)
.innerJoin(push.lastModifiedBy, admin)

로그를 확인했을 때 createdBy와 lastModifiedBy에서 동일한 식별 변수를 중복 선언하여 발생한 문제로 파악했습니다.

(ORM 6 버전이 ORM 5 버전보다 엄격하게 유효성 검사를 진행)

 

.innerJoin(push.createdBy, createdBy)
.innerJoin(push.lastModifiedBy, modifiedBy)

이를 해결하기 위해 조건에 따라 별도의 조인을 사용하여 createdBy와 lastModifiedBy가 서로 다른 admin일 때도 각각의 조인이 이루어지도록 수정했습니다.

 

[트러블 슈팅] FunctionArgumentException

public static NumberTemplate<Double> calculateDistance(double lat, double lon){
	return Expressions.numberTemplate(
		Double.class,
		"(6731 * ACOS(COS(RADIANS({0})) * COS(RADIANS(AES_DECRYPT(FROM_BASE64(latitude), {2}))) * COS(RADIANS(AES_DECRYPT(FROM_BASE64(longitude), {2})) - RADIANS({1})) + SIN(RADIANS({0})) * SIN(RADIANS(AES_DECRYPT(FROM_BASE64(latitude), {2})))))",
		lat,
		lon,
		encryptKey
	);
}

문제 상황 : 2km 이내의 플레이스 목록을 조회하는 쿼리에서 "FunctionArgumentException" 예외가 발생한다.

이전 버전에서 정상적으로 실행되었던 쿼리가 마이그레이션 이후 API를 호출하는 단계에서 예외가 발생했습니다.

 

예외 메시지 중 "Parameter 1 of function 'radians()' has type 'NUMERIC', but argument is of type 'java.lang.String' mapped to 'VARCHAR'"를 확인할 수 있습니다. 이는 radians() 함수의 매개변수 타입이 'NUMERIC'이어야 하는데, 'VARCHAR' 타입('java.lang.String')이 들어와서 발생한 문제로 추측했습니다.

 

해결 방안 : 형변환을 통해 NUMERIC 데이터로 변환한다. (CAST)

public static NumberTemplate<Double> calculateDistance(double lat, double lon){
	return Expressions.numberTemplate(
		Double.class,
		"(6731 * ACOS(COS(RADIANS({0})) * COS(RADIANS(CAST(AES_DECRYPT(FROM_BASE64(latitude), {2}) AS DOUBLE))) * COS(RADIANS(CAST(AES_DECRYPT(FROM_BASE64(longitude), {2}) AS DOUBLE)) - RADIANS({1})) + SIN(RADIANS({0})) * SIN(RADIANS(CAST(AES_DECRYPT(FROM_BASE64(latitude), {2}) AS DOUBLE)))))",
		lat,
		lon,
		encryptKey
	);
}

거리를 계산하는 메소드에서 사용하는 RADIANS()는 암호화된 위도 경도를 복호화해서 값을 넣는데 이 과정에서 NUMERIC이 아닌 VARCHAR 데이터로 인식하여 발생한 문제로 추측했습니다. (ORM 6 버전이 ORM 5 버전보다 엄격하게 유효성 검사를 진행)

 

문제를 해결하기 위해 DB 형변환 함수인 CAST 함수를 사용했습니다. 이를 통해 복호화된 문자열 데이터를 숫자로 변환하여 RADIANS() 함수가 올바르게 처리할 수 있게 했습니다.

 

[트러블 슈팅] Deprecated MySQL8Dialect

문제 상황 : MySQL8Dialect가 deprecated되어 등록된 사용자 정의 함수를 사용할 수 없다.

MySQL8Dialect가 deprecated되었고, registerFunction은 obsolete되었습니다. 

 

Migrate Hibernate 5 to 6 with Spring Boot 2.7.x to 3 해당 hibernate 커뮤니티 글에 따르면 FunctionContributor의 "contributeFunctions" 메소드를 오버라이딩하여 문제를 해결했다는 내용이 작성되어있습니다.

 

해결 방안 : CustomFunctionContributor를 통해 DB 함수를 등록한다.

CustomFunctionContributor
application.yml

SqmFunctionRegistry을 생성 후 "register()" 메소드를 통해 사용할 DB 함수를 등록했습니다. 등록한 함수를 사용하기 위해서는 properties 또는 yml 파일에 "spring.jpa.properties.hibernate.function_contributor" 에 작성된 CustomFunctionContributor을 등록해 사용할 수 있습니다.