QueryDsl을 사용해 검색 API를 개선하던 중, QueryDsl을 이용한 페이징을 찾다가 나온 내용들을 정리하는 글이다.
Pageable의 size값 limit가 없다
page가 0부터 시작한다
→ 만약 2페이지를 클릭했을 때, pageable의 page는 0부터 시작되기 때문에 처리 전 1을 빼줘야 하는 상황이 발생
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 쿼리를 이야기하는지 궁금할 수도 있기 떄문에 짚고 넘어가자
Jpa에서 제공해주는 Page를 사용하게 되면 totalCount 쿼리를 매번 호출하게 된다. 데이터가 많아질수록 이 작업은 부담스러운 작업이다.
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);
}
카운트 쿼리가 생략가능한 경우 생략해서 처리할 수 있도록 하자
아래와 같은 경우, 카운트 쿼리가 생략 가능하다.
return PageableExecutionUtils.getPage(fetch, pageable, count::fetchCount);
위와 같이 PageableExecutionUtils.getPage()로 return을 하는데, 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());
}
offset=0
) 일 경우content.size()
를 total Count로 리턴한다.offset=0
)이 아니면서, 한 페이지 사이즈 크기보다 쿼리로 가져온 data 개수가 작을 경우offset + content.size()
가 곧 total 개수참고
번외 - 페이지 성능 개선 (추후에 참고하면 좋을 것)