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) 에서 어떤 차이가 있기에 버전업 이후에 이슈가 발생했는지 확인해보자.
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


QueryDSL 5.0.0 버전의 NumberConversions.java 의 일부이다.
args (DB에서 가져온 Tuple) 에서 Money 타입 value 들이 BigDecimal 로 들어온 것이 보인다.
변환해야할 객체 (1번째 사진에서 71줄: type) 은 Money 타입이고
DB에서 가져온 타입은 BigDecimal 이어서,
BigDecimal -> Money 형변환을 하려고 했으나
2번째 사진처럼 Custom Type (Money) 는 별도로 형변환을 지원하지 않아 에러가 발생한다.
args 배열 (DB에서 가져온 Tuple)에 정상적으로 Money 타입으로 읽어와졌다.
더욱 자세하게는, QueryLoader 에서 resultRow 를 얻을 때 AttributeConverter 가 적용되었었다. (6.3.1 에서는 적용되지 않음)

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
}
}
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)

hibernate 6.3.1 Final - BinaryArithmeticExpression.java 코드의 일부이다.
resultType 이 Money 여야 하는데, 제대로 불러오지 못해 NPE 가 발생.
Hibernate 의 내부 동작이 완전히 바뀌었다.
5.6.15 에는 없는 로직이 6.3.1 에서 실행되면서 발생한 것.
내부적으로 fetch() > getResultList() > list() 메소드를 타는데
5.6.15 : AbstractProducedQuery.list()
6.3.1 : AbstractSelectionQuery.list() // 5.6.15 에는 없는 클래스
를 타도록 수정된 것
// 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 을 처리할 수 있게 된다.



Claim 도메인의 엔티티 구조는 이런 식으로 되어있다.
자식 엔티티를 읽은 후, field 의 값을 수정하면 DB에 반영되지 않는다.
별도 에러 없이 그냥 DB 반영이 안됨.
테스트 코드 통해 발견된 이슈.
도무지 모르겟다.. 다만, Child 객체를 디버깅을 해보면
field = 수정된값
Parent.field = null
으로 반영되고 있다.
그리고 hibernate 는 Parent 를 참조하여 update 하는 것 같다.. (뇌피셜)

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