개발

JPQL 로 Pageable 객체 사용 시 주의할 점

모달조아 2023. 7. 11. 16:00

개요

Spring Data JPA Repository 에 Pageable 객체를 파라미터로 전달하면 별도의 쿼리를 작성하지 않아도 페이징을 위한 쿼리를 만들어준다. 전통적인 오프셋 기반의 페이징 처리를 할때는 전체 데이터 수가 알아야하기에 Spring Data JPA 에서는 Page 객체를 반환하는 방식을 주로 이용한다. 이때, 전체 데이터 수를 알아야하므로 페이징 쿼리 외에도 추가적으로 count 쿼리가 나간다. 이 과정에서 JPQL 을 사용하여 페이징 쿼리를 작성할 때, 주의할 점이 있어 공유하고자 글을 작성한다.

 

주의할 점

결론부터 이야기하자면 count 쿼리가 @Query 로 작성한 JPQL 을 기반으로 나간다는 것을 인지하고 있어야한다는 것이다. 특히, fetch join 을 사용할 경우 예외가 발생할 확률이 높은데 그 이유를 한번 알아보자.

@Query("SELECT r FROM Review r JOIN FETCH r.member WHERE r.seat.id = :seatId AND r.published IS TRUE")
Page<Review> findAllWithFetchMemberBySeatIdAndPublishedTrue(@Param("seatId") Long seatId, Pageable pageable);

위 쿼리는 특정 좌석의 발행된 후기(Review)들을 찾아서 페이징하는 쿼리이다. 동시에 후기 엔티티와 다대일 관계를 가지는 회원 엔티티를 fetch join 하고 있다.

위 쿼리를 실행하면 제대로 동작하지 않고, java.lang.IllegalStateException: Failed to load ApplicationContext 예외가 발생한다.

java.lang.IllegalArgumentException: Count query validation failed for method public abstract org.springframework.data.domain.Page com.goodseats.seatviewreviews.domain.review.repository.ReviewRepository.findAllWithFetchMemberBySeatIdAndPublishedTrue(java.lang.Long,org.springframework.data.domain.Pageable)!

원인은 맨 처음 말했듯 count 쿼리가 @Query 로 작성한 JPQL 을 기반으로 나가기 때문이다. @Query 에서 fetch join 을 사용하였기에 count 쿼리에도 fetch join 이 사용된다. fetch join 을 사용하면 객체 그래프를 탐색하여 결과를 엔티티로 반환한다. 이때, count 쿼리는 반환 값이 Long 이어야 하는데 엔티티로 반환되니 예외가 발생하는 것이다.

 

해결 방법

크게 4가지의 방법이 있다.

1. @Query 에서 별도의 count 쿼리를 명시

@Query 어노테이션에서 별도로 count 쿼리를 지정해줄 수 있다.

@Query(value = "SELECT r FROM Review r JOIN FETCH r.member WHERE r.seat.id = :seatId AND r.published IS TRUE",
	countQuery ="SELECT count(r) FROM Review r WHERE r.seat.id = :seatId AND r.published IS TRUE" )
Page<Review> findAllWithFetchMemberBySeatIdAndPublishedTrue(@Param("seatId") Long seatId, Pageable pageable);

아래에서 count 쿼리가 지정해준대로 정상적으로 나가는 것을 확인할 수 있다.

 

2. EntityGraph 사용

@EntityGraph 를 사용하면 left outer join 으로 연관된 엔티티를 함께 가져오므로 fetch join 을 사용하지 않아도 된다. 따라서 count 쿼리가 자동 생성될 때도 문제가 생기지 않는다. left outer join 을 이용하므로 필요하지 않은 정보들도 가져온다는 점을 잊지말자.

 

 

3. 반환 값을 엔티티가 아닌 DTO 로 함

아래와 같이 반환을 DTO 로 하는 것이다. DTO 로 직접 반환하게 되면 굳이 fetch join 해줄 필요가 없으므로 문제가 생기지 않는다. 다만, repository 에서부터 DTO 를 반환하게 되면 서비스단에서 유연하게 엔티티 로직을 처리하기 어려워지므로 개인적으로는 선호하지 않는다.

@Query(value = "SELECT new com.goodseats.seatviewreviews.domain.review.model.dto.response.ReviewsElementResponse(r.id, r.title, r.score, r.viewCount, r.member.nickname) FROM Review r WHERE r.seat.id = :seatId AND r.published IS TRUE")
Page<ReviewsElementResponse> findAllWithFetchMemberBySeatIdAndPublishedTrue(@Param("seatId") Long seatId, Pageable pageable);

 

4. QueryDSL 사용

QueryDSL 을 사용하면 자동으로 count 쿼리가 만들어지지 않고, 직접적으로 명시해야만 한다. 그러므로 앞서 1번에서 count 쿼리를 명시했던 것과 같은 원리로 문제가 발생하지 않는다.