[Spring Boot 버전업] hibernate 5.6.15 -> hibernate 6.3.1 Final 버전업 이슈

조갱·2025년 12월 7일

이슈 해결

목록 보기
17/17

개요

Shopby 의 주문파트에서는 BigDecimal 을 Wrapping 한, Money 라는 Custom Type 을 정의하여 사용하고 있다.

class Money : Comparable<Money>, Number {
    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    constructor(value: BigDecimal) {
        this.value = value
    }

    @JsonValue
    var value: BigDecimal = BigDecimal.ZERO
        private set
        
    ...
}

이 Money 타입들은 Request, Response 뿐만 아니라
Entity, JPA, QueryDSL 등 DB 에서도 사용된다.

당연하게도, MySQL 에는 Money 라는 타입이 없기에 이슈가 발생할 수 있지만
버전업 이전 (Hibernate 5.6.15) 과 후 (6.3.1 Final) 에서 어떤 차이가 있기에 버전업 이후에 이슈가 발생했는지 확인해보자.

발생한 이슈 목록

Unsupported target type : Money

언제 발생했는가?

QueryDSL 에서 sum 쿼리 생성할 때
Money 타입을 sum 하지 못해 발생. (단순 Select 는 가능)

private fun getQuery(params: SearchParams): JPQLQuery<ResponseModel> {
    return from(sales).select(
        Projections.constructor(
            ResponseModel::class.java,
            sales.orderCnt.sum(), // 여기서 발생
            ...
        )
    )
}

에러 메시지

org.springframework.dao.InvalidDataAccessApiUsageException: Unsupported target type : Money

	at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:371)
	at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:246)
	at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:550)
	at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)
	at org.springframework.dao.support.DataAccessUtils.translateIfNecessary(DataAccessUtils.java:335)
	at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:152)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)
	at org.springframework.data.jpa.repository.support.CrudMethodMetadataPostProcessor$CrudMethodMetadataPopulatingMethodInterceptor.invoke(CrudMethodMetadataPostProcessor.java:164)
	at

6.3.1 Final - 어디에서 에러가 발생할까?


QueryDSL 5.0.0 버전의 NumberConversions.java 의 일부이다.
args (DB에서 가져온 Tuple) 에서 Money 타입 value 들이 BigDecimal 로 들어온 것이 보인다.

변환해야할 객체 (1번째 사진에서 71줄: type) 은 Money 타입이고
DB에서 가져온 타입은 BigDecimal 이어서,
BigDecimal -> Money 형변환을 하려고 했으나

2번째 사진처럼 Custom Type (Money) 는 별도로 형변환을 지원하지 않아 에러가 발생한다.

5.6.15 에서는 왜 동작했는가?

args 배열 (DB에서 가져온 Tuple)에 정상적으로 Money 타입으로 읽어와졌다.

더욱 자세하게는, QueryLoader 에서 resultRow 를 얻을 때 AttributeConverter 가 적용되었었다. (6.3.1 에서는 적용되지 않음)

해결한 방법

  • QueryDsl 7.0 버전업
  • Kotlin 확장함수를 통해 sum() -> sumAsTyped() 로 변경
 inline fun <reified S> NumberExpression<S>.sumAsTyped(): TypeWrapper<BigDecimal, S>
    where S : Number, S : Comparable<S> {

    return TypeWrapper(this.sumAggregate().castToNum(BigDecimal::class.java), S::class.java) { input ->
        val adjustedInput = input.orZero()

        when(S::class) {
            Money::class -> Money(adjustedInput)
            Int::class -> adjustedInput.toInt()
            Long::class -> adjustedInput.toLong()
            Float::class -> adjustedInput.toFloat()
            Double::class -> adjustedInput.toDouble()
            Rate::class -> Rate(adjustedInput)
            Percent::class -> Percent(adjustedInput)
            BigDecimal::class -> adjustedInput
            else -> {
                logger.error("QueryDSL TypeWrapper unsupported type: ${S::class.java}")
                throw NcpInternalException("Unsupported type for TypeWrapper conversion: ${S::class.java}")
            }
        } as S
    }
}

"this.resultType" is null

언제 발생했는가?

QueryDSL 에서 add, subtract 를 포함한 쿼리를 fetch 할 때
Money 타입을 add, subtract 하지 못해 발생. (단순 Select 는 가능)

private fun getFetchData(params: SearchParams): List<FetchedResponseModel> {
    return from(sales).select(
        Projections.constructor(
            FetchedResponseModel::class.java,
            sales.mallAdditionalDiscountAmt.add(sales.mallProductCouponDiscountAmt).add(sales.mallFreeGiftAmt),
            ...
        )
    ).fetch() // 여기서 발생
}

에러 메시지

java.lang.NullPointerException: Cannot invoke "org.hibernate.metamodel.mapping.BasicValuedMapping.getJdbcMapping()" because "this.resultType" is null
	at org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression.resolveSqlSelection(BinaryArithmeticExpression.java:72)

6.3.1 Final - 어디에서 에러가 발생할까?

hibernate 6.3.1 Final - BinaryArithmeticExpression.java 코드의 일부이다.

resultType 이 Money 여야 하는데, 제대로 불러오지 못해 NPE 가 발생.

5.6.15 에서는 왜 동작했는가?

Hibernate 의 내부 동작이 완전히 바뀌었다.
5.6.15 에는 없는 로직이 6.3.1 에서 실행되면서 발생한 것.

내부적으로 fetch() > getResultList() > list() 메소드를 타는데
5.6.15 : AbstractProducedQuery.list()
6.3.1 : AbstractSelectionQuery.list() // 5.6.15 에는 없는 클래스

를 타도록 수정된 것

해결한 방법

  • Hibernate Type Contributors 추가
// TypeContributorList 가 Deprecated + removal 예정이지만, 동작을 위해서는 필요하기에 우선 사용함.
@file:Suppress("DEPRECATION", "removal")

// package, import 정의

@Configuration
class CustomHibernateTypeConfig() : HibernatePropertiesCustomizer {
    override fun customize(hibernateProperties: MutableMap<String?, Any?>) {
        hibernateProperties.put(
            TYPE_CONTRIBUTORS,
            TypeContributorList {
                val typeContributor = TypeContributor { contributions, _ ->
                    val registry = contributions.typeConfiguration.basicTypeRegistry
                    registry.register(MoneyUserType(), MoneyUserType.MONEY_CLASS_NAME)
                }
                listOf(typeContributor)
            }
        )
    }
}

class MoneyUserType : UserType<Money> {
    companion object {
        val MONEY_CLASS_NAME = Money::class.jvmName
    }

    override fun getSqlType(): Int {
        return Types.DECIMAL
    }

    override fun returnedClass(): Class<Money>? {
        return Money::class.java
    }

    @Throws(HibernateException::class)
    override fun equals(x: Money?, y: Money?): Boolean {
        // 각자 상황에 맞게 구현
    }

    @Throws(HibernateException::class)
    override fun hashCode(x: Money?): Int {
        // 각자 상황에 맞게 구현
    }

    @Throws(HibernateException::class, SQLException::class)
    override fun nullSafeGet(
        rs: ResultSet,
        position: Int,
        session: SharedSessionContractImplementor,
        owner: Any?,
    ): Money {
        // 각자 상황에 맞게 구현
    }

    @Throws(HibernateException::class, SQLException::class)
    override fun nullSafeSet(
        st: PreparedStatement, value: Money?, index: Int, session: SharedSessionContractImplementor,
    ) {
        // 각자 상황에 맞게 구현
    }

    @Throws(HibernateException::class)
    override fun deepCopy(value: Money?): Money? {
        return value
    }

    override fun isMutable(): Boolean {
        return false
    }

    @Throws(HibernateException::class)
    override fun disassemble(value: Money): Serializable? {
        // 각자 상황에 맞게 구현
    }

    @Throws(HibernateException::class)
    override fun assemble(cached: Serializable, owner: Any): Money? {
        // 각자 상황에 맞게 구현
    }

    @Throws(HibernateException::class)
    override fun replace(original: Money, target: Money, owner: Any): Money {
        // 각자 상황에 맞게 구현
    }
}

위 설정을 추가하고 나면, BaseSqmToSqlAstConverter.java 의 getExpressionType 을 통해 Money Type 을 처리할 수 있게 된다.

Configuration 적용 전 :

Configuration 적용 후 :

자식 Entity Class 의 open var 필드에 값이 안써지는 현상

언제 발생했는가?


Claim 도메인의 엔티티 구조는 이런 식으로 되어있다.
자식 엔티티를 읽은 후, field 의 값을 수정하면 DB에 반영되지 않는다.

에러 메시지

별도 에러 없이 그냥 DB 반영이 안됨.
테스트 코드 통해 발견된 이슈.

이슈 원인

도무지 모르겟다.. 다만, Child 객체를 디버깅을 해보면

field = 수정된값
Parent.field = null

으로 반영되고 있다.
그리고 hibernate 는 Parent 를 참조하여 update 하는 것 같다.. (뇌피셜)

해결한 방법

override var 를 굳이 사용할 필요가 없어서 제거함.

profile
A fast learner.

0개의 댓글