데이터가 많아질수록 정렬과 페이징은 필수적인 기능이다.
이번 장에서는 QueryDSL을 활용해 정렬(orderBy)과 페이징(offset, limit)을 처리하는 방법과 페이징 실제 예시를 알아볼 것이다.
1. 정렬 — orderBy()
다음 조건으로 회원을 정렬한다고 가정하자.
- 나이는 내림차순(desc)
- 이름은 오름차순(asc)
- 이름이 없는(null) 회원은 마지막에 정렬(nullsLast)
@Test
public void sort() {
em.persist(new Member(null, 100));
em.persist(new Member("member5", 100));
em.persist(new Member("member6", 100));
List<Member> result = queryFactory
.selectFrom(member)
.where(member.age.eq(100))
.orderBy(member.age.desc(), member.username.asc().nullsLast())
.fetch();
Member member5 = result.get(0);
Member member6 = result.get(1);
Member memberNull = result.get(2);
assertThat(member5.getUsername()).isEqualTo("member5");
assertThat(member6.getUsername()).isEqualTo("member6");
assertThat(memberNull.getUsername()).isNull();
}
정렬 관련 주요 메서드
| asc() | 오름차순 정렬 |
| desc() | 내림차순 정렬 |
| nullsFirst() | null 값을 가장 먼저 정렬 |
| nullsLast() | null 값을 가장 뒤로 정렬 |
asc().nullsLast() 조합을 자주 사용한다.
특히 nullable 컬럼이 있는 경우, 예상치 못한 null 정렬을 방지할 수 있다.
2. 페이징 — offset(), limit()
대량 데이터를 한 번에 다 가져오는 것은 비효율적이다.
offset()과 limit()을 이용하면 간단하게 페이지 단위로 데이터를 나눌 수 있다
@Test
public void paging1() {
List<Member> result = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1) // 0부터 시작 (zero index)
.limit(2) // 최대 2건 조회
.fetch();
assertThat(result.size()).isEqualTo(2);
}
- offset(1) → 첫 번째 결과를 건너뛴다.
- limit(2) → 이후 최대 2건만 조회한다.
이렇게 하면,
1번째 결과를 제외하고 2번째~3번째 데이터를 반환한다.
3. 전체 건수 조회 — fetchResults()
fetchResults()는 전체 개수까지 함께 조회하고 싶을 때 사용된다.
@Test
public void paging2() {
QueryResults<Member> queryResults = queryFactory
.selectFrom(member)
.orderBy(member.username.desc())
.offset(1)
.limit(2)
.fetchResults();
assertThat(queryResults.getTotal()).isEqualTo(4);
assertThat(queryResults.getLimit()).isEqualTo(2);
assertThat(queryResults.getOffset()).isEqualTo(1);
assertThat(queryResults.getResults().size()).isEqualTo(2);
}
fetchResults()는 다음 두 쿼리를 내부적으로 실행한다:
- 데이터 조회용 쿼리
- 전체 개수(count) 계산용 쿼리
4. 성능 주의점
fetchResults()는 편리하지만, 성능 이슈가 발생할 수 있다.
왜냐하면 count 쿼리를 자동으로 만들 때, 원본 쿼리의 모든 조인을 그대로 따라가기 때문이다.
예를 들어 여러 테이블을 조인하는 복잡한 조회 쿼리의 경우,
count 쿼리에는 조인이 필요 없는데도 그대로 실행되어 성능이 떨어질 수 있다.
따라서 실제 프로젝트에선 count 전용 쿼리를 별도로 작성하는 방식을 권장한다.
5. 실제 예시 — 회원 100명을 10명씩 페이징하기
회원이 100명 있고, 한 페이지당 10명씩 보여주고 싶다고 하자.
다음은 QueryDSL + Spring Data JPA의 Pageable을 활용한 실무형 예제다.
public Page<Member> findMembers(Pageable pageable) {
List<Member> content = queryFactory
.selectFrom(member)
.orderBy(member.id.asc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
long total = queryFactory
.select(member.count())
.from(member)
.fetchOne();
return new PageImpl<>(content, pageable, total);
}
예를 들어 다음과 같이 호출하면:
PageRequest pageable = PageRequest.of(2, 10);
Page<Member> page = memberRepository.findMembers(pageable);
→ 내부적으로 아래 SQL이 실행된다.
select *
from member
order by id asc
limit 10 offset 20;
즉 사용자는 2페이지를 눌렀을때 11번부터 20번까지 확인할 수 있는 것이다.
마무리하며
정렬(orderBy)과 페이징(offset, limit)은 QueryDSL에서 자주 사용하는 기본 기능이다.
특히 페이징의 경우 fetchResults()는 간편하지만,
성능 최적화를 위해 count 쿼리를 별도로 작성하는 것이 실무 표준이다.
결국 핵심은 다음 한 줄로 요약된다.
"조회 쿼리는 명확하게, count 쿼리는 가볍게
이것이 QueryDSL 페이징의 핵심이다."
감사합니다.
'QueryDSL' 카테고리의 다른 글
| [QueryDSL] 결과 조회 (0) | 2025.10.26 |
|---|---|
| [QueryDSL] 검색 조건 쿼리 (0) | 2025.10.26 |
| [QueryDSL] QueryDSL과 빠른 세팅 방법 (0) | 2025.10.26 |
