본문 바로가기
Spring/JPA

Querydsl Paging & Count Query 최적화

by 흑시바 2023. 11. 11.

fetchResults() - Deprecated 

기존에는 fetchResults()를 사용하여 페이징 처리할 때 개발자가 별도의 count 쿼리를 작성하지 않아도 돼서 편리했다.

하지만 Querydsl 5.0 버전부터  fetchResults는 Deprecated 되었다. 

 

fetchResults()는 개발자가 작성한 select 쿼리를 기반으로 count 용 쿼리를 내부에서 만들어서 실행한다.

단순 쿼리에서는 동작에 문제가 없었으나, 다중 그룹 쿼리(group By) 등 복잡한 쿼리에서는 잘 작동하지 않는다.

사용자들에게는 fetch() 후 결과를 count 하여 사용하라는 대안을 제시한다.

 

Querydsl에서 페이징 처리를 할 때 다음과 같이 보통 Pageable를 파라미터로 받고 pageSize, offset 정보를 매핑해서 정보를 가지고 온다.

 

    public Page<Disease> findAllByCodeAndNameAndType(String code, String name, DiseaseType type, Pageable pageable) {
        List<Disease> content = from(disease)
                .where(searchByCode(code),
                        searchByName(name),
                        searchByType(type))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
                
  ...

 

반환하는 PageImpl에는 전체 데이터 수(total) 정보를 포함해야 하는데, 결과(content)로 나오는 정보는 limit 처리되었기 때문에 List의 size()를 활용할 수 없다. 그러므로 count 쿼리를 별도로 작성해서 사용해야 한다.

 

그런데 무조건 count를 수행하면 굳이 count 쿼리가 필요 없는 상황에도 조회가 될 수도 있다.

 

Spring Data에서 제공하는 PageableExecutionUtils 유틸리티 클래스의 getPage()를 사용하면 필요한 경우에만 count 쿼리를 동작시키도록 하여 최적화할 수 있다.

 

PageableExecutionUtils.getPage()를 살펴보자.

 

public static <T> Page<T> getPage(List<T> content, Pageable pageable, LongSupplier totalSupplier) {
	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. 첫 번째 페이지이면서 content 크기가 한 페이지의 사이즈보다 작은 경우
2. 마지막 페이지일 때 즉, getOffset이 0이 아니면서, content 크기가 한 페이지의 사이즈보다 작은 경우

 

위 2가지 경우는 totalSupplier 대신 content의 size와 offset 값으로 total 값을 대신한다는 걸 알 수 있다.

 

이를 활용해서 전체 코드를 작성하면 다음과 같다.

 

public Page<Disease> findAllByCodeAndNameAndType(String code, String name, DiseaseType type, Pageable pageable) {
	List<Disease> content = from(disease)
			.where(searchByCode(code),
					searchByName(name),
					searchByType(type))
			.offset(pageable.getOffset())
			.limit(pageable.getPageSize())
			.fetch();

	JPQLQuery<Long> countQuery = from(disease)
			.select(disease.count())
			.where(searchByCode(code),
					searchByName(name),
					searchByType(type));

	return PageableExecutionUtils.getPage(content, pageable, () -> countQuery.fetchOne());
}

 

Querydsl과 페이징을 사용할 때 PageableExecutionUtils를 사용하여 성능 이득을 노려보자.

 

댓글