👥💡 Querydsl 페이징: fetchResults()가 비권장(Deprecated)된 이유와 실무 정석 패턴

2026. 2. 14. 17:12·프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트

1️⃣ 예전 방식: fetchResults()

강의나 책에서 자주 보던 코드

QueryResults<MemberTeamDto> results = queryFactory
    .select(...)
    .from(member)
    .offset(pageable.getOffset())
    .limit(pageable.getPageSize())
    .fetchResults();

List<MemberTeamDto> content = results.getResults();
long total = results.getTotal();

내부 동작

fetchResults()는 내부적으로 두 개의 쿼리를 실행한다.

  1. content 조회 쿼리
  2. count(*) 조회 쿼리

한 번에 처리해주기 때문에 코드가 짧고 편하다.

2️⃣ 문제점 (왜 Deprecated 되었는가)

❌ 1. Querydsl 5.x에서 Deprecated

공식적으로 fetchResults(), fetchCount()는 Deprecated 되었다.

이유는:

복잡한 쿼리에서 잘못된 count 쿼리가 생성될 위험

❌ 2. Count 쿼리 최적화가 불가능

예를 들어:

.leftJoin(member.team, team)

이 조인이 content 조회에는 필요하지만
count에는 필요 없을 수 있다.

 

하지만 fetchResults()를 사용하면
count 쿼리에도 그대로 join이 포함된다.

 

실제 실행 SQL:

select count(*)
from member
left join team ...

👉 count에 필요 없는 join까지 수행
👉 성능 저하 가능성

❌ 3. fetch join + 페이징 충돌

JPA 규칙상:

fetch join + pagination = 메모리 페이징

fetchResults()는 내부에서 count 쿼리까지 자동 생성하기 때문에
fetch join과 함께 사용하면 예측하기 어려운 동작이 발생할 수 있다.

실무에서 매우 위험한 패턴이다.

3️⃣ 실무 정석 패턴: content / count 분리

① Content 쿼리

List<Article> content = queryFactory
        .selectFrom(article)
        .leftJoin(article.author, user).fetchJoin()
        .where(...)
        .offset(pageable.getOffset())
        .limit(pageable.getPageSize())
        .fetch();

② Count 쿼리

Long total = queryFactory
        .select(article.count())
        .from(article)
        .where(...)
        .fetchOne();

③ Page 객체 생성

return new PageImpl<>(content, pageable, total);

4️⃣ 왜 분리하는가?

✅ 1. Count 최적화 가능

Content 쿼리:

.fetchJoin()

Count 쿼리:

.from(article) // 불필요한 join 제거

👉 count 전용 최적화 가능
👉 대용량 테이블에서 성능 차이 매우 큼

✅ 2. fetch join 안전하게 사용 가능

  • content 쿼리 → fetch join 사용 가능
  • count 쿼리 → fetch join 제거

명확하게 역할 분리 가능

✅ 3. 실무 표준 패턴

실무 Querydsl 페이징의 90% 이상이:

  1. content 쿼리
  2. count 쿼리
  3. PageImpl 생성

Spring Data JPA 공식 문서도 동일한 패턴을 권장한다.

5️⃣ 한 단계 더 발전: PageableExecutionUtils

더 발전된 방식:

return PageableExecutionUtils.getPage(
    content,
    pageable,
    () -> queryFactory
            .select(article.count())
            .from(article)
            .where(...)
            .fetchOne()
);

장점

마지막 페이지일 경우 count 쿼리를 실행하지 않는다.

예:

  • page size = 10
  • 조회 결과 = 3개

👉 이미 마지막 페이지
👉 굳이 count 쿼리 실행 안 함
👉 쿼리 1번 절약

실무에서 매우 유용한 최적화 방식이다.

'프로젝트 > 스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글

👥💡 조회수 동시성 문제 해결(원자적)  (0) 2026.02.14
👥💡 Spring Data JPA + Querydsl 커스텀 Repository 구조 정리  (0) 2026.02.14
👥🚨 수정시간 및 수정하기 작성자 권한 변경  (0) 2026.02.14
👥🚨 수정은 됐는데 화면에는 “실패”라고 뜬다?  (1) 2026.02.07
👥💡 N+1 리팩토링 과정  (1) 2026.02.07
'프로젝트/스프링 부트 3 백엔드 개발자 되기 Blog + 선착순 강의 프로젝트' 카테고리의 다른 글
  • 👥💡 조회수 동시성 문제 해결(원자적)
  • 👥💡 Spring Data JPA + Querydsl 커스텀 Repository 구조 정리
  • 👥🚨 수정시간 및 수정하기 작성자 권한 변경
  • 👥🚨 수정은 됐는데 화면에는 “실패”라고 뜬다?
hak0622
hak0622
개발하면서 “이게 뭐지?”라는 순간마다 궁금한 점을 바탕으로 정리한 개발 블로그입니다.
  • hak0622
    궁금한 개발 이야기 Why?
    hak0622
  • 전체
    오늘
    어제
    • 분류 전체보기 (68)
      • 공부 (36)
        • 1. 자바 ORM 표준 JPA 프로그래밍 - 기본.. (35)
        • 시험 (1)
      • 프로젝트 (32)
        • 스프링 부트 3 백엔드 개발자 되기 Blog + .. (32)
  • 인기 글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
hak0622
👥💡 Querydsl 페이징: fetchResults()가 비권장(Deprecated)된 이유와 실무 정석 패턴
상단으로

티스토리툴바