사내에서 특정 Entity에 value 클래스를 도입해 사용하고 있다.
이 중에서 자리수가 중요한 사업자등록번호나 휴대폰 번호 같은 값은 value 클래스를 통해 유효성을 보장한다.
하지만 Kotlin JDSL 환경에서는 이러한 value 클래스를 직접 사용하기 위해 커스텀 시리얼라이저를 등록해야 한다.
https://kotlin-jdsl.gitbook.io/docs/ko-1/faq/how-do-i-use-kotlin-value-class
Kotlin value class 를 사용하려면 어떻게 해야할까요? | Kotlin JDSL
Last updated 5 months ago
kotlin-jdsl.gitbook.io
Kotlin JDSL 문서에 따르면, value 클래스의 필드를 사용하려면 기본적으로 시리얼라이저 등록이 필요하다.
하지만 더 나아가 value 클래스에서 like 같은 조건을 사용하고 싶다면 추가 작업이 필요하다.
value 클래스 필드 Like 검색 조건 사용하기
예제: Name이라는 value 클래스
Name이라는 value 클래스가 있다고 가정해보자. 일반적인 JDSL에서는 아래와 같은 코드는 지원되지 않는다:
path(Company::name)(Name::name).like(pattern)
이는 value 클래스가 런타임에서 언박싱되기 때문에, 적절한 JPQL로 변환될 수 없기 때문이다.
그렇다면 아래처럼 value 클래스에서도 자연스럽게 like를 지원하도록 만들어보자
path(Company::name).like(pattern)
나만의 DSL 구현
JDSL 가이드북을 보면 다음과 같이 안내한다.

JDSL 가이드를 참고하여 커스텀 DSL을 구현해보았다.
1. 커스텀 Predicate 정의
먼저 value 클래스용 Predicate를 만든다. 아래는 Name 타입의 필드에 like를 지원하는 DSL 함수이다
class CustomJpql : Jpql() {
fun Path<Name>.like(value: String): Predicate {
return CmsJpqlNameLike(this.toExpression(), Expressions.value(value))
}
}
data class CustomJpqlNameLike internal constructor(
val value: Expression<Name>,
val pattern: Expression<String>,
) : Predicate
2. JPQL 직렬화기 작성
다음으로, 위에서 만든 Predicate를 JPQL로 변환할 시리얼라이저를 구현한다.
class CustomJpqlNameLikeSerializer: JpqlSerializer<CustomJpqlNameLike> {
override fun handledType(): KClass<CustomJpqlNameLike> {
return CustomJpqlNameLike::class
}
override fun serialize(part: CustomJpqlNameLike, writer: JpqlWriter, context: RenderContext) {
val delegate = context.getValue(JpqlRenderSerializer)
delegate.serialize(part.value, writer, context)
writer.write(" ")
writer.write("LIKE")
writer.write(" ")
delegate.serialize(part.pattern, writer, context)
}
}
3. 시리얼라이저 Bean 등록
JDSL에서는 스프링 환경에서 시리얼라이저를 Bean으로 등록하면 자동으로 RenderContext에 추가된다.
JDSL은 스프링부트 환경을 지원하고 있어, 시리얼라이저를 Bean으로 등록하면 KotlinJdslAutoConfiguration에 의해 RenderContext를 자동으로 구성해준다.
@Configuration(proxyBeanMethods = false)
class JdslConfig {
@Bean
fun jpqlSerializer(): JpqlSerializer<*> {
return ValueClassAwareJpqlValueSerializer(JpqlValueSerializer())
}
@Bean
fun jpqlLikeSerializer(): JpqlSerializer<*> {
return CustomJpqlNameLikeSerializer()
}
}
실 사용 예제
JdslExecutor를 매번 JpaRepository에 상속받아 사용하기는 귀찮아 공용으로 사용할 수 있는 JdslRepo를 만들어 사용 중이다.
이곳에 내가 만든 CustomJpql로 교체시켜준다.
@Component
class JdslRepository(
private val jdslJpqlExecutor: KotlinJdslJpqlExecutorImpl,
private val entityManager: EntityManager,
private val jpqlRenderContext: JpqlRenderContext
) {
fun <T : Any> findOne(init: CustomJpql.() -> JpqlQueryable<SelectQuery<T>>): T? {
return entityManager.createQuery(init(CustomJpql()).toQuery(), jpqlRenderContext)
.apply { maxResults = 1 }
.resultList
.firstOrNull()
}
fun <T : Any> findAll(offset: Int?, limit: Int?, init: CustomJpql.() -> JpqlQueryable<SelectQuery<T>>): List<T> {
return jdslJpqlExecutor.findAll(CustomJpql, offset = offset, limit = limit, init)
.filterNotNull()
}
fun <T : Any> findAll(init: CustomJpql.() -> JpqlQueryable<SelectQuery<T>>): List<T> {
return jdslJpqlExecutor.findAll(CustomJpql, offset = null, limit = null, init)
.filterNotNull()
}
fun <T : Any> findPage(pageable: Pageable, init: CustomJpql.() -> JpqlQueryable<SelectQuery<T>>): Page<T> {
val page = jdslJpqlExecutor.findPage(CmsJpql, pageable, init)
return PageImpl(page.filterNotNull(), pageable, page.totalElements)
}
}
확장 함수 추가 구현
like뿐만 아니라 querydsl과 같이 startsWith, endsWith 같은 함수도 필요하다면 CustomJpql에 함수를 추가하여
검색 조건을 유연하게 작성할 수 있다:
적용
fun findAllBy(keyword: String?, pageable: Pageable): Page<Company> {
return jdslRepository.findPage(pageable) {
selectFrom(entity(Company::class))
.whereAnd(nameOrBusinessRegistrationNoLike(keyword))
.orderBy(path(Company::modifiedAt).desc())
}
}
private fun CmsJpql.nameOrBusinessRegistrationNoLike(keyword: String?): Predicatable? {
return if (keyword.isNullOrBlank()) {
null
} else {
path(Company::name).contains(keyword).or( // 적용된 DSL
path(Company::businessRegistrationNo).startsWith(keyword) // 적용된 DSL
)
}
}
마무리
위 방식으로 Kotlin JDSL에서 value 클래스와 DSL을 활용해 유연하고 가독성 높은 JPQL 쿼리를 작성할 수 있다.
실제 프로젝트에서 적용해보며 필요한 기능을 지속적으로 확장해 나가보면 좋겠다.