PageableExecutionUtils.getPage로 페이징 성능 개선하기

2025. 10. 24. 23:30·Spring

✅ 개요

Querydsl은 페이징 처리를 위해 `PageImpl()`의 최적화 버전인 `PageableExecutionUtils` 클래스의 정적 메서드 `getPage()`를 지원합니다. ` PageableExecutionUtils.getPage()`는 어떻게 페이징 쿼리를 최적화하는지 분석하고 정리하려고 합니다.

 

✅ PageableExecutionUtils

먼저 `PageableExecutionUtils` 클래스 내부 코드를 살펴보았습니다.

PageableExecutionUtils

내부적으로 엄청나게 복잡한 로직으로 이루어져 있지는 않아서 다행이었습니다. 하나씩 이해하면 원리를 완벽히 이해하기에 큰 어려움은 없을 것 같습니다.

 

➡️ isPartialPage

`getPage` 메서드는 먼저 `isPartialPage`인지 확인하고 있습니다. 여기서 false를 반환하면 바로 카운트 쿼리를 실행하러 갑니다.

isPartialPage

조건은 간단했습니다. 조회하려는 페이지 크기가 조회된 데이터의 개수보다 큰지 비교하고 있습니다. 

예를 들어 `pageSize=10`으로 조회했을 때 조회된 데이터(`content`)가 7개이면 이는 마지막 페이지일 것입니다.

즉 조회하려는 페이지 크기가 조회된 데이터의 개수보다 크다는 것은 마지막 페이지 또는 결과가 없다고 볼 수 있고,

반대로 조회된 데이터가 같거나 많은 경우는 중간 페이지 일 것이기 때문에 다음 페이지가 있을 수 있다고 보고 카운트 쿼리를 실행해 전체 페이지 수를 계산해야 합니다.

 

➡️ isFirstPage

`isFirstPage`는 offset이 0인지, 즉 첫 페이지인지 확인하고 있습니다.

isFirstPage

바깥 if문 `isPartialPage` 조건과 결합하여 보면 첫 페이지이면서 결과 개수가 pageSize보다 적은 경우를 의미합니다. 예를 들어 `page=0`, `size=10`으로 조회했을 때 결과가 6개이면, 이미 전체 데이터가 한 페이지 안에 다 존재한다라고 이해할 수 있습니다.

이 경우 조회된 데이터 개수가 곧 `total`을 의미하기 때문에 굳이 카운트 쿼리를 실행하지 않고 `content.size()`를 전달하는 것입니다.

➡️ !content.isEmpty()

이 조건은 이번 페이지 결과 개수가 pageSize보다 작고(`isPartialPage`), 첫 페이지는 아닐 때(`isFirstPage`) 실행됩니다. 이때 결과가 하나라도 있다면 카운트 쿼리 없이 반환합니다.

예를 들어 `page=2`, `size=10`으로 조회했을 때 결과가 5개이면, 2페이지를 마지막 페이지라고 볼 수 있습니다. 즉 카운트 쿼리 필요 없이 현재까지의 offset에 남은 데이터 개수를 더하면 `total`을 계산할 수 있는 것입니다.

➡️ 그 외

그 외의 경우는 모두 추가 카운트 쿼리를 실행합니다. 즉 size만큼 데이터가 조회됐으며 마지막 페이지인지도 알 수 없다면 전체 개수를 계산으로 알 수가 없기 때문에 카운트 쿼리가 필요하게 됩니다.

 

➕ PageImpl 생성자

`PageImpl`의 생성자도 살펴보았는데 특이한 부분이 있었습니다. 전달받은 `total`을 그대로 사용하지 않고 무언가 추가적인 계산을 하는 것입니다.

PageImpl

Javadoc 설명을 요약해 보면 마지막 페이지일 경우 content의 길이를 고려해 total이 조정될 수 있다고 하며, 이는 불일치를 완화하기 위함이라고 합니다.

  • `filter(it -> !content.isEmpty())` : 현재 페이지의 결과가 비어있으면 스킵
  • `filter(it -> it.getOffset() + it.getPageSize() > total)`  : `offset + size`가 total보다 크다면, total 값이 실제보다 작게 전달된 것 같다고 판단
  • `map(it -> it.getOffset() + content.size())` : 보정된 total 계산
  • `orElse(total)` : 그 외 전달받은 total 그대로 사용

예시 1) - 정상적인 경우

`page=0`, `size=10`이며 `content.size=10`, `total=45`인 경우 `offset(0) + pageSize(10) <= total(45)` 이므로 그대로 total 사용 가능

예시 2) - 마지막 페이지

`page=4`, `size=10`이며 `content.size=5`, `total=45`인 경우 `offset(40) + pageSize(10) > total(45)` 이므로 total을 `offset(40) + content.size(5) = 45`로 보정

 

예시 3) - 잘못된 total 전달

`page=2`, `size=10`이며 `content.size=10`, `total=25`인 경우  `offset(20) + pageSize(10) > total(25)` 이므로 total을 `offset(20) + content.size(10) = 30`으로 보정

 

즉, 현재 페이지의 끝이 total보다 크다면 total을 현재 페이지 끝 위치로 보정하는 것이며 이는 total이 잘못 계산되어 전달된 경우를 방지하기 위해 현재 페이지 정보를 기반으로 total 값을 보정하는 방어적 코드를 포함한다고 볼 수 있습니다.

 

📌 정리

`PageableExecutionUtils`은 어떻게 페이징 쿼리를 최적화하는지 알아보았습니다. 최적화가 가능하다고 해서 항상 이 기능을 사용하는 것이 맞는지는 모르겠지만, 스프링에서 개발자를 위해 어떤 기능을 어떻게 제공하는지 알 수 있었습니다.

 

'Spring' 카테고리의 다른 글

Spring AI 개발 일지 (2) - OpenAI 사용해 후기 요약 구현해보기  (0) 2025.11.19
Spring AI 개발 일지 (1) - Spring AI 소개와 핵심 모델  (0) 2025.11.19
Spring 비동기로 이메일 전송하기  (0) 2025.10.21
Spring AbstractRequestLoggingFilter로 RequestBody 로깅 찍기  (0) 2025.10.14
[데브코스] Spring + Toss payments로 결제 기능 개발하기  (0) 2025.10.13
'Spring' 카테고리의 다른 글
  • Spring AI 개발 일지 (2) - OpenAI 사용해 후기 요약 구현해보기
  • Spring AI 개발 일지 (1) - Spring AI 소개와 핵심 모델
  • Spring 비동기로 이메일 전송하기
  • Spring AbstractRequestLoggingFilter로 RequestBody 로깅 찍기
이런개발
이런개발
geun-00의 흔적 보관소
  • 이런개발
    내일이 기대되는 오늘
    이런개발
  • 전체
    오늘
    어제
    • 분류 전체보기 N
      • 백엔드 면접
      • SQL N
        • SUM, MAX, MIN
        • SELECT
        • GROUP BY
        • JOIN
      • Spring
      • JPA
      • 트러블슈팅
      • Infra
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    스프링
    오블완
    백엔드 면접
    토스 페이먼츠
    티스토리챌린지
    데브코스
    raid
    JPA
    자바
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
이런개발
PageableExecutionUtils.getPage로 페이징 성능 개선하기
상단으로

티스토리툴바