프로젝트를 진행하다 보면 다양한 조건을 기반으로 데이터를 검색해야 할 때가 많습니다. 예를 들어, title, englishLevel, city와 같은 조건을 이용해 Circle 데이터를 검색할 때, 이들 조건이 null일 경우 해당 조건을 무시하고 싶을 수 있습니다. 이런 경우 Kotlin JDSL에서는 null 값 조건을 쉽게 무시하는 방법을 제공하며, 이번 글에서는 이를 어떻게 적용할 수 있는지 설명드리겠습니다.

 

문제 상황

아래와 같은 쿼리를 작성했다고 가정해보겠습니다.

override fun findCirclesByPagination(pageable: Pageable, request: CircleSearchRequest?)
: Page<CirclePageResponse?> {
    return kotlinJdslJpqlExecutor.findPage(pageable) {
        selectNew<CirclePageResponse>(
                path(Circle::id),
                path(Circle::thumbnailUrl),
                path(Circle::title),
                path(Circle::introduction),
                path(Member::profile).`as`(expression("leaderProfile")),
                path(Member::nickname).`as`(expression("leaderName")),
                path(Circle::englishLevel),
                path(Circle::city),
                path(Circle::capacity),
                path(Circle::totalView),
                path(Circle::totalLike),
        ).from(
                entity(Circle::class),
                join(Circle::leader)
        ).whereAnd(
                path(Circle::title).like("%${request?.title}%"),
                path(Circle::englishLevel).eq(request?.level),
                path(Circle::city).eq(request?.city)
        )
    }
}

여기서 우리는 title, englishLevel, city 세 가지 조건을 사용하고 있습니다. 하지만 이 필드들 중 하나라도 null일 경우 쿼리에서 해당 조건을 무시해야 할 때 어떻게 해야 할까요?

 

해결 방법: 조건을 let으로 감싸기

Kotlin JDSL에서는 whereAnd 블록 내에서 조건을 동적으로 추가할 수 있습니다. let 함수를 활용해 각 조건이 null이 아닐 때만 적용되도록 설정해보겠습니다.

 

override fun findCirclesByPagination(pageable: Pageable, request: CircleSearchRequest?)
: Page<CirclePageResponse?> {
    return kotlinJdslJpqlExecutor.findPage(pageable) {
        selectNew<CirclePageResponse>(
                path(Circle::id),
                path(Circle::thumbnailUrl),
                path(Circle::title),
                path(Circle::introduction),
                path(Member::profile).`as`(expression("leaderProfile")),
                path(Member::nickname).`as`(expression("leaderName")),
                path(Circle::englishLevel),
                path(Circle::city),
                path(Circle::capacity),
                path(Circle::totalView),
                path(Circle::totalLike),
        ).from(
                entity(Circle::class),
                join(Circle::leader)
        ).whereAnd(
                request?.title?.let { path(Circle::title).like("%$it%") },
                request?.level?.let { path(Circle::englishLevel).eq(it) },
                request?.city?.let { path(Circle::city).eq(it) }
        )
    }
}

 

코드 설명

  1. 조건 추가를 위한 let 사용: request?.title, request?.level, request?.city가 각각 null이 아닐 때만 let 블록 내부의 조건이 실행되어 쿼리에 추가됩니다.
  2. null 값 무시: title, level, city 중 하나라도 null이면 해당 조건은 자동으로 쿼리에서 제외됩니다.

동작 방식 예시

  • title만 null인 경우: englishLevel과 city만 조건으로 포함됩니다.
  • 모든 값이 null인 경우: 조건 없이 전체 결과가 반환됩니다.

이를 통해 null 조건을 유연하게 무시하고 원하는 조건에 따라 필터링된 결과를 반환받을 수 있습니다.

 

마무리

Kotlin JDSL에서 조건을 동적으로 설정하는 것은 상당히 직관적이며, let 함수와 같은 Kotlin의 기능을 활용해 쿼리의 가독성을 높일 수 있습니다. 이 방식은 특히 검색 기능을 구현할 때 유용하며, nullable 필드를 조건으로 사용할 때 코드의 안정성과 효율성을 높여줍니다.

이번 글에서는 Kotlin JDSL을 사용해 조건이 null인 경우 쿼리에서 해당 조건을 제외하는 방법을 살펴보았습니다. 앞으로 JDSL을 활용해 다양한 쿼리를 동적으로 구성할 때 큰 도움이 되길 바랍니다.

'Kotlin-Jdsl' 카테고리의 다른 글

Kotlin JDSL 도입기: Page 타입 응답값 반환하기  (0) 2024.11.03

최근 프로젝트에서 Kotlin JDSL을 도입하며, 특히 페이지네이션된 데이터를 반환할 때 몇 가지 문제에 직면했습니다. 자료가 부족하거나 오래되어 직접 구현하기 쉽지 않았지만, 공식 문서와 GitHub 테스트 코드를 참고하여 문제를 해결할 수 있었습니다. 이번 글에서는 해결 과정을 코드와 함께 공유하고자 합니다.

문제 해결 과정: Page 타입 응답 구현

페이지네이션된 데이터를 반환하기 위해 KotlinJdslJpqlExecutor의 findPage 메서드와 selectNew를 사용했습니다. selectNew 메서드는 엔티티가 아닌 사용자 정의 DTO로 데이터를 매핑할 수 있어 매우 유용합니다. 아래는 구현한 코드입니다

@Repository
class CustomCircleRepositoryImpl(
    private val kotlinJdslJpqlExecutor: KotlinJdslJpqlExecutor
) : CustomCircleRepository {

    override fun findCirclesByPagination(pageable: Pageable): Page<CirclePageResponse?> {
        return kotlinJdslJpqlExecutor.findPage(pageable) {
            selectNew<CirclePageResponse>(
                path(Circle::id),
                path(Circle::thumbnailUrl),
                path(Circle::title),
                path(Circle::introduction),
                path(Member::profile).`as`(expression("leaderProfile")),
                path(Member::nickname).`as`(expression("leaderName")),
                path(Circle::englishLevel),
                path(Circle::city),
                path(Circle::capacity),
                path(Circle::totalView),
                path(Circle::totalLike)
            ).from(
                entity(Circle::class),
                join(Circle::leader)
            )
        }
    }
}

코드 설명

  1. CustomCircleRepositoryImpl 클래스: KotlinJdslJpqlExecutor를 주입받아 findCirclesByPagination 메서드를 구현한 커스텀 리포지토리입니다.
  2. findCirclesByPagination 메서드: Pageable 객체를 인자로 받아 Page<CirclePageResponse?> 타입의 응답을 반환합니다. 이 메서드는 JPA의 Page 기능을 활용해 페이지네이션된 데이터를 반환하도록 설계되었습니다.
  3. selectNew 메서드: Kotlin JDSL의 핵심 기능 중 하나로, CirclePageResponse라는 사용자 정의 DTO에 엔티티 데이터를 매핑합니다. path 메서드를 통해 각 필드를 선택하며, DTO 필드와 일치하도록 매핑합니다. Member 엔티티에서 리더의 프로필과 닉네임을 가져오기 위해 join을 사용했습니다.

DTO 설계와 발생한 이슈

페이지네이션을 위해 사용하는 CirclePageResponse는 다음과 같은 구조를 가지고 있습니다:

@Schema(description = "서클 페이지 조회 응답")
data class CirclePageResponse(
    val id: UUID,
    val thumbnailUrl: String? = null,
    val title: String,
    val introduction: String = "",
    val leaderProfile: String? = null,
    val leaderName: String,
    val englishLevel: EnglishLevel,
    val city: City,
    val capacity: Int,
    val totalView: Int,
    val totalLike: Int,
    val likedByMe: Boolean = false
)

이슈: selectNew 메서드의 제한 사항

selectNew를 사용할 때, DTO의 생성자 파라미터에 정확히 일치하는 개수와 순서대로 값을 할당해야 합니다. likedByMe 필드는 기본값이 false로 설정되어 있었지만, selectNew는 기본값을 무시하고 모든 파라미터에 값을 전달하도록 강제합니다. 이로 인해 초기 코드에서 오류가 발생했습니다.

해결 방법

이 문제를 해결하기 위해 likedByMe 필드를 생성자 밖으로 이동하여 기본값을 설정하도록 변경했습니다. 수정된 DTO는 다음과 같습니다.

@Schema(description = "서클 페이지 조회 응답")
data class CirclePageResponse(
    val id: UUID,
    val thumbnailUrl: String? = null,
    val title: String,
    val introduction: String = "",
    val leaderProfile: String? = null,
    val leaderName: String,
    val englishLevel: EnglishLevel,
    val city: City,
    val capacity: Int,
    val totalView: Int,
    val totalLike: Int
) {
    val likedByMe: Boolean = false
}

이제 selectNew는 DTO의 모든 생성자 파라미터에 값을 할당할 필요가 없어졌습니다. likedByMe 필드는 클래스 바디에 정의되어 기본값 false로 설정됩니다. 이로써 selectNew의 제한 사항을 우회하면서 원하는 데이터를 매핑할 수 있었습니다.

'Kotlin-Jdsl' 카테고리의 다른 글

Kotlin JDSL에서 Null 값을 가진 조건 무시하기  (1) 2024.11.07

+ Recent posts