QueryDsl을 사용하다 보면 조건에 따라서 Join이 필요한 경우도 있고, 그렇지 않은 경우도 있을 수 있다.
Join자체를 동적으로 적용하는 것은 QueryDsl에서 지원하지도 않고 들어본 적도 없다.
이러한 한계점을 우회하여 어떻게 해결하면 좋을지 알아 보자.
필자가 처했던 상황을 예시를 들어 보겠다.
후기와 후기 이미지 테이블이 존재하고, 후기를 기준으로 페이징하여 조회해야 한다.
이 때 조건이 붙게 되는데, 이미지가 있는 후기만 조회하거나 모든 후기를 조회해야 하거나 두 가지 경우이다.
이 경우 이미지가 있는 후기만을 조회해야 한다면 Join이 필수적인 상황이지만, 그렇지 않다면 Join이 필요하지 않다.
그래서 동적으로 Join을 적용해 보고자 고민하게 되었다.
가장 간단한 방법이다.
Join을 하는 메소드와 하지 않는 메소드로 분리하여 필요에 따라 호출을 달리하는 방법이다.
Join을 하는 메소드
public Slice<ReviewDetailDto> findPhotoReviews(ReviewSearchCond cond, Pageable pageable) {
List<Review> reviews = query
.select(review)
.distinct()
.from(review)
.innerJoin(review.reviewImages, reviewImage)
.where(
review.place.id.eq(cond.getPlaceId()),
roomIdEq(cond.getRoomId()),
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.fetch();
}
Join을 하지 않는 메소드
public Slice<ReviewDetailDto> findReviews(ReviewSearchCond cond, Pageable pageable) {
List<Review> reviews = query
.select(review)
.distinct()
.from(review)
.where(
review.place.id.eq(cond.getPlaceId()),
roomIdEq(cond.getRoomId()),
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.fetch();
}
메소드를 두 개로 분리했던 것을 하나의 메소드 안에서 if문을 사용해 동적으로 Join을 적용하면 된다.
public Slice<ReviewDetailDto> findReviews(ReviewSearchCond cond, Pageable pageable) {
List<Review> reviews;
JPAQuery<Review> selectQuery = query
.select(review)
.distinct()
.from(review)
if(cond.getHasPhoto()){
selectQuery = selectQuery.innerJoin(review.reviewImages, reviewImage)
}
selectQuery
.where(
review.place.id.eq(cond.getPlaceId()),
roomIdEq(cond.getRoomId()),
)
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.fetch();
}
매우 간단한 방법이지만 썩 눈에 들어오지는 않는다.
조건문이 Join을 위한 조건문이라는 것이 한 눈에 보이지도 않고 왜 Join이 동적으로 필요한지도 잘 표현되지 않는다.
필자는 더 가독성을 높이면서도 동적으로 Join을 적용하는 방법이 있지 않을까 고민했다.
고민 끝에 메소드를 통해서 Join 적용을 위임하면 되지 않을까 싶었다.
private <T> JPAQuery<T> innerJoinIfPhotoOnly(JPAQuery<T> selectQuery, boolean photoOnly) {
return photoOnly ? selectQuery.innerJoin(review.reviewImages,reviewImage) : selectQuery;
}
위와 같은 메소드를 하나 만들었다.
쿼리를 넘겨주고 조건에 사용할 인자를 받는 형태이다.
그리고 메소드 안에서 조건을 확인하고 필요에 따라 Join을 수행해서 쿼리를 반환하거나 Join이 필요 없다면 그대로 반환한다.
public Slice<ReviewDetailDto> findReviews(ReviewSearchCond cond, Pageable pageable) {
List<Review> r;
JPAQuery<Review> selectQuery = query
.select(review)
.distinct()
.from(review)
r = innerJoinIfPhotoOnly(selectQuery, cond.getHasPhoto())
.where(
review.place.id.eq(cond.getPlaceId()),
roomIdEq(cond.getRoomId())
)
.orderBy(reviewSort(cond))
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.fetch();
}
들여쓰기를 적절히 활용해서 기존에 Join절이 위치해야 할 곳에서 메소드를 호출하여 위화감을 줄였다.
또, 메소드를 사용했기 때문에 메소드명을 자유롭게 사용하여 Join이 어떤 상황에서 적용되는지도 명확히 표현할 수 있게 되었다.
위와 같은 방법을 사용하면 동적으로 Join을 적용하면서도 가독성이 떨어지지 않도록 할 수 있지 않을까 싶다.
같은 메소드를 사용했지만, 요청 조건에 따라 동적으로 Join이 적용되어 다르게 쿼리가 나가는 것을 확인할 수 있다.
( 위에서는 설명을 위해 쿼리를 간략화 했지만, 실제로는 다른 테이블과의 Join도 포함한 쿼리를 사용했습니다. )
동적으로 Join을 적용하기 위해서 메소드와 들여쓰기를 최대한 활용해 위화감을 줄이는 형태로 구현해 보았다.
이렇게 사용하는 코드를 본적이 없기 때문에, 사용하지 않는 데에는 다 이유가 있지 않을까 싶기도 하다.
실제로 메소드를 여러개로 분리하여 사용하는 것이 유지보수적인 측면이나 명확성에서 더 장점이 있을 것 같다.
하지만 필자의 개인적인 의견으로는 필요에 따라서 적절하게 적용한다면 괜찮은 방법이 될 수 있다고 생각한다.