페이징 성능: 카운트 최적화·키셋 페이징
대부분의 목록 API는 Page<T>를 쓰지만, 조인/필터가 복잡해지면 count가 병목이 됩니다. 또한 무한 스크롤에서는 키셋(Keyset) 페이징이 오프셋보다 훨씬 효율적입니다. 이 글은 실무에서 바로 적용하는 3가지 전략을 정리합니다.
전략 1) countQuery 분리
@Query(
value = "select o from Order o join o.member m where m.status = :s",
countQuery = "select count(o) from Order o join o.member m where m.status = :s"
)
Page<Order> findByMemberStatus(@Param("s") Status s, Pageable pageable);
- 목록 쿼리에서 불필요한
fetch join/정렬/컬럼을 빼고 카운트 전용으로 단순화 - 정렬 컬럼에 인덱스 부여(
order by created_at desc등)
전략 2) Slice로 가볍게
Slice<Order> findTop20ByStatusOrderByIdDesc(Status s, Pageable pageable);
무한 스크롤 UI라면 전체 개수는 의미가 없습니다. Slice는 다음 페이지가 있는지만 알려주므로 count 쿼리를 생략하고 반응성이 개선됩니다.
전략 3) 키셋(Keyset) 페이징
// 마지막 id를 커서처럼 사용
@Query("select o from Order o where (:cursor is null or o.id < :cursor) order by o.id desc")
List<Order> next(@Param("cursor") Long cursor, Pageable pageable);
- 장점: 대용량에서도 빠르고 안정적(인덱스 범위 스캔)
- 주의: 정렬 기준이 단조 증가(id/createdAt)해야 함. 정렬 컬럼에 인덱스 필수.
- 정렬 다중 컬럼일 때는 (createdAt, id) 복합 인덱스로 중복 방지.
QueryDSL 예시
public Slice<OrderView> next(OrderCond cond, Long cursor, int size) {
QOrder o = QOrder.order; QMember m = QMember.member;
List<OrderView> rows = query
.select(Projections.constructor(OrderView.class, o.id, m.name, o.createdAt))
.from(o).join(o.member, m)
.where(
cond.status() != null ? o.status.eq(cond.status()) : null,
cursor != null ? o.id.lt(cursor) : null
)
.orderBy(o.id.desc())
.limit(size + 1)
.fetch();
boolean hasNext = rows.size() > size;
if (hasNext) rows.remove(size);
return new SliceImpl<>(rows, PageRequest.of(0, size), hasNext);
}
대용량 페이징 팁
- 정렬 컬럼(및 보조키)에 적절한 인덱스 부여
- 선택 컬럼 최소화(DTO 투영)로 I/O 감소
- 필터 조건의 선택도가 낮다면 부분 인덱스/파티셔닝 고려
FAQ
Q. 카운트를 대략으로 빨리 구할 수 있나요?
A. 일부 DB의 통계 테이블로 추정값을 얻을 수 있지만(예: PG의 reltuples), JPA 표준 바깥이며 정확도가 필요 없는 분석 화면에만 제한하세요.
Q. 키셋에서 특정 페이지로 점프?
A. 오프셋/카운트를 섞어 하이브리드로 구현하거나, 기간/검색어로 좁힌 뒤 키셋을 적용하세요.
⏪ 이전 글 보기: @Transactional 전파/고립수준 이해
👉 2편: 메서드명 쿼리 vs @Query vs QueryDSL 비교
'Java & Spring' 카테고리의 다른 글
| springdoc-openapi로 API 문서 1분 셋업 (0) | 2025.10.05 |
|---|---|
| Jackson 직·역직렬화 어노테이션 핵심(@Json*) (0) | 2025.10.05 |
| @Transactional 전파/고립수준 이해 (1) | 2025.10.03 |
| 메서드명 쿼리 vs @Query vs QueryDSL 비교 (0) | 2025.10.02 |
| 연관관계 주인/지연로딩 N+1 체크리스트 (0) | 2025.10.02 |