티스토리 뷰

Spring/JPA

[Querydsl] PageableExecutionUtils

snail voyager 2026. 1. 18. 22:57
728x90
반응형

PageableExecutionUtils란?

Spring Data 라이브러리(org.springframework.data.support)에서 제공하는 헬퍼 클래스로,

Page<T> 객체(페이징 결과)를 생성할 때 불필요한 Count 쿼리 실행을 생략(Skip)하여 성능을 최적화해 줍니다.

왜 PageableExecutionUtils가 필요한가?

Spring Data JPA의 기본 페이징은 항상 2개의 쿼리를 실행

1) content 조회
2) select count(*) ...

  • 마지막 페이지라면?
  • 요청한 size보다 결과가 적다면?
  • 더 이상 페이지가 없다는 것이 이미 명확하다면?

→ count 쿼리는 쓸데없는 비용

❌ 잘못된 방식

long count = query.fetchCount();	//무조건 count 쿼리 실행됨
List<Member> content = query.fetch();
return new PageImpl<>(content, pageable, count);
count 쿼리를 실행하지 않고도 total을 추론할 수 있으면, 실행하지 말자

 

현재 페이지 결과 count 쿼리 실행 여부
content.size < pageSize ❌ 실행 안함
첫 페이지 & 결과 없음
마지막 페이지가 명확
애매한 경우 ✅ 실행

✅ 올바른 방식

1) content 조회 (LIMIT/OFFSET)
2) 결과 개수 보고 마지막 페이지인지 판단
3) 마지막이면 → count 안 함
4) 아니면 → count 쿼리 실행

import com.querydsl.jpa.impl.JPAQuery;
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.stereotype.Repository;

import java.util.List;

import static com.example.entity.QMember.member;

@Repository
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {

    private final JPAQueryFactory queryFactory;

    public MemberRepositoryCustomImpl(JPAQueryFactory queryFactory) {
        this.queryFactory = queryFactory;
    }

    @Override
    public Page<MemberDto> searchPage(MemberSearchCondition condition, Pageable pageable) {
        
        // 1. 컨텐츠 조회 쿼리 (데이터를 가져옴)
        List<MemberDto> content = queryFactory
                .select(new QMemberDto(member.username, member.age))
                .from(member)
                .where(usernameEq(condition.getUsername()),
                       ageEq(condition.getAge()))
                .offset(pageable.getOffset()) // 페이지 번호
                .limit(pageable.getPageSize()) // 페이지 사이즈
                .fetch();

        // 2. 카운트 쿼리 (실행하지 않고 쿼리 객체만 생성!)
        // 중요: 여기서 fetch()나 fetchOne()을 호출하면 안 됩니다.
        JPAQuery<Long> countQuery = queryFactory
                .select(member.count())
                .from(member)
                .where(usernameEq(condition.getUsername()),
                       ageEq(condition.getAge()));

        // 3. PageableExecutionUtils 사용
        // 마지막 인자로 함수(Supplier)를 넘깁니다. ::fetchOne
        return PageableExecutionUtils.getPage(content, pageable, countQuery::fetchOne);
    }
    
    // ... Predicate 메서드 생략 ...
}

PageImpl vs PageableExecutionUtils

특징 new PageImpl(...) PageableExecutionUtils.getPage(...)
Count 쿼리 실행 시점 개발자가 미리 실행해서 **숫자(long)**로 넘겨줘야 함. 내부 로직에 따라 필요할 때만 실행함.
인자 (List content, Pageable pageable, long total) (List content, Pageable pageable, LongSupplier totalSupplier)
성능 무조건 Count 쿼리가 실행됨 (비효율적일 수 있음). 상황에 따라 Count 쿼리 생략 (최적화됨).
권장 여부 간단한 테스트나 전체 개수가 중요하지 않을 때 사용. 실무 프로덕션 코드에서 권장.

 

 

728x90
반응형

'Spring > JPA' 카테고리의 다른 글

[Querydsl] JPASQLQuery addFlag() 를 사용해서 SQL 힌트 적용  (0) 2024.12.02
JPA 영속성 관리  (0) 2020.05.24
JPA (Java Persistence API)  (0) 2020.05.24
반응형
300x250