Spring Boot

Spring Boot 페이지네이션(Page, Pageable)

최-코드 2024. 9. 8. 22:35

상황1 : 페이지네이션을 사용하여 쿼리를 날리면 count 쿼리가 나가는 것을 발견했다. 

 

cf) 실무에서 페이징 쿼리를 작성할 때, 데이터를 조회하는 쿼리는 여러 테이블을 조인해야 하지만, count 쿼리는 조인이 필요없는 경우도 있다. 만약 자동화된 count 쿼리는 원본 쿼리와 같이 모두 조인을 하기 때문에 성능이 안 나올 수 있다. 따라서 count 쿼리에 조인이 필요없는 성능 최적화가 필요하다면, count 전용 쿼리를 별도로 작성하면 된다. (꼭 페이지를 통한 페이지네이션을 할 때)

 

해결법1 : Page 객체에는 전체 페이지를 구하는 함수가 있다. 따라서 count 쿼리를 통해 전체 페이지를 구하는 것이었다. 따라서 전체 페이지에 대해 알 필요가 없다면, 그냥 repository 클래스에서 Page 대신 List로 바로 받으면 count 쿼리가 발생하지 않는다.

 

cf) pageable를 인자로 넘겨주면 @Query에 아무것도 안 붙여도 내부적으로 쿼리에 order by와 limit이 붙게 된다.

 

상황2 : limit ?1, ?2와 같이 사용하는 것을 보았다. 이는 ?1개의 행을 건너 띈 ?2개의 행을 가져온다는 소리이다. 이는 ?1개의 행을 읽고 ?2개의 행을 가져오는 것이기 때문에 만약 ?1의 값이 백만이 넘는다면, 결국 백만 개의 행을 조회하는 것과 같다.

 

cf) limit은 정렬하는 과정에서 limit에 지정된 수에 도달하면 바로 반환한다.

 

해결법2 : 커서 방식 페이지네이션 사용하기

  • 커서 방식은 페이지 번호와 페이지 수가 존재하여 페이지를 클릭하는 방식이 아니라 밑으로 드래그를 하면 추가적인 데이터가 날아오는 방식을 말한다.
  • 예를 들어 최신순으로 정렬된 게시글을 불러오면 맨 마지막으로 불러온 게시글의 id값 다음을 불러오면 된다.
  • 이 때 QueryDsl을 사용하면 동적 쿼리에 대해 유연하게 대응할 수 있다. 예를 들어 처음 게시글을 불러올 때 마지막 게시글 id값은 null이 될 것이다. 이에 대해 동적으로 where 절을 구성하면 된다.

Controller

@GetMapping
public ResponseEntity<?> getNovels(
        @RequestParam(name = "lastId", required = false) Long lastId) {
    List<NovelResponseDto> novelResponseDtos = novelService.findNovelsByLatest(lastId);
    return ResponseDto.onSuccess(novelResponseDtos);
}

 

Repository

@Override
public List<NovelResponseDto> findOrderByNovelId(Long lastId) {
    return jpaQueryFactory
            .select(new QNovelResponseDto(
                    novelEntity.novelId, novelEntity.title,
                    novelEntity.startSentence, novelEntity.likeCount))
            .from(novelEntity)
            .where(novelIdLt(lastId))
            .orderBy(novelEntity.novelId.desc())
            .limit(PAGE_SIZE)
            .fetch();
}

private BooleanExpression novelIdLt(Long novelId) {
        return novelId != null ? novelEntity.novelId.lt(novelId) : null;
}