QueryDsl Paging

haaaalin·2023년 4월 25일
3

QueryDsl을 사용해 검색 API를 개선하던 중, QueryDsl을 이용한 페이징을 찾다가 나온 내용들을 정리하는 글이다.

Custom PageRequest

왜 Custom PageRequest가 필요할까?

  1. Pageable의 size값 limit가 없다

  2. page가 0부터 시작한다

    → 만약 2페이지를 클릭했을 때, pageable의 page는 0부터 시작되기 때문에 처리 전 1을 빼줘야 하는 상황이 발생

✏️ Custom PageRequest

public class PageRequest {

    private int page = 1;
    private int size = 10;
    private Direction direction = Direction.DESC;

    public void setPage(int page) {
        this.page = page <= 0 ? 1 : page;
    }

    public void setSize(int size) {
        int DEFAULT_SIZE = 10;
        int MAX_SIZE = 50;
        this.size = size > MAX_SIZE ? DEFAULT_SIZE : size;
    }

    public void setDirection(Direction direction) {
        this.direction = direction;
    }

    public org.springframework.data.domain.PageRequest of() {
        return org.springframework.data.domain.PageRequest.of(page - 1, size, direction, "create_date");
    }
}


페이징 & Count 쿼리

페이징을 하는데 왜 count 쿼리를 이야기하는지 궁금할 수도 있기 떄문에 짚고 넘어가자

Jpa에서 제공해주는 Page를 사용하게 되면 totalCount 쿼리를 매번 호출하게 된다. 데이터가 많아질수록 이 작업은 부담스러운 작업이다.

QueryDsl 페이징 쿼리

QueryDsl로 페이징 쿼리를 작성할 때 count 쿼리를 분리해서 사용하면, 성능이 개선되는 경우가 있다고 한다.

QuerydslRepositorySupport를 이용한 Paging
getQuerydsl().applyPagination() 스프링 데이터가 제공하는 페이징을 QueryDsl로 편리하게 변환이 가능하다고 한다.

하지만 count 쿼리를 분리해서 작성하는 게 여러 방면에서 좋다고 생각한다.

join이나 다른 작업이 원본 쿼리에 포함돼서 조회할 경우에 해당한다.

원본 쿼리에 있는 작업들을 count하는 과정에서도 불필요하게 수행하고, count되기때문에 카운트 쿼리를 분석해서 최적화를 기대할 수 있다고 한다.

추후에 join을 포함하는 등, 복잡한 원본 쿼리를 사용할 가능성이 높아 미리 count 쿼리를 분리해놓기로 했다.

@Override
    public Page<Project> searchProjectsWith(Pageable pageable, String projectName, String content) {
        List<Project> fetch = queryFactory
                .selectFrom(project).where(projectNameEq(projectName), contentEq(content))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();

        JPQLQuery<Project> count = queryFactory
                .select(project)
                .from(project)
                .where(projectNameEq(projectName), contentEq(content));

        return PageableExecutionUtils.getPage(fetch, pageable, count::fetchCount);
    }

Count Query 최적화

카운트 쿼리가 생략가능한 경우 생략해서 처리할 수 있도록 하자

아래와 같은 경우, 카운트 쿼리가 생략 가능하다.

  • 페이지 시작이면서, 컨텐츠 사이즈가 페이지 사이즈보다 작을 때
  • 마지막 페이지일 때(offset + 컨텐츠 사이즈를 더해서 전체 사이즈를 구한다)
return PageableExecutionUtils.getPage(fetch, pageable, count::fetchCount);

위와 같이 PageableExecutionUtils.getPage()로 return을 하는데, getPage()가 카운트 쿼리를 생략가능한 경우를 처리해준다.


✏️ PageableExecutionUtils.getPage()

public static <T> Page<T> getPage(List<T> content, Pageable pageable, LongSupplier totalSupplier) {

    Assert.notNull(content, "Content must not be null!");
    Assert.notNull(pageable, "Pageable must not be null!");
    Assert.notNull(totalSupplier, "TotalSupplier must not be null!");

    if (pageable.isUnpaged() || pageable.getOffset() == 0) {

        if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) {
            return new PageImpl<>(content, pageable, content.size());
        }

        return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
    }

    if (content.size() != 0 && pageable.getPageSize() > content.size()) {
        return new PageImpl<>(content, pageable, pageable.getOffset() + content.size());
    }

    return new PageImpl<>(content, pageable, totalSupplier.getAsLong());
}
  1. 페이징 정보가 없거나, 데이터 시작 지점(offset=0) 일 경우
  • 페이징 정보가 없거나, 쿼리로 가져온 data 개수가 한 페이지 사이즈 크기보다 작을 경우 → 그대로 content.size()를 total Count로 리턴한다.
  • 그 외는 페이징 쿼리 실행, total 개수 반환
  1. 데이터 시작 지점(offset=0)이 아니면서, 한 페이지 사이즈 크기보다 쿼리로 가져온 data 개수가 작을 경우
  • 쿼리를 날리지 않고도 offset + content.size()가 곧 total 개수


참고

번외 - 페이지 성능 개선 (추후에 참고하면 좋을 것)

profile
한 걸음 한 걸음 쌓아가자😎

0개의 댓글