페이지네이션(pagination)은 큰 목록을 한 번에 다 주지 않고 조각으로 나눠 주는 방법이다. 크게 오프셋 방식(offset-based, 몇 번째부터 몇 개)과 커서 방식(cursor-based, 이 항목 다음부터 몇 개)으로 나뉜다.
방식 비교
| 페이지 번호 | limit·offset | 커서 | |
|---|---|---|---|
| 요청 | ?page=3 | ?limit=20&offset=40 | ?cursor=eyJpZCI6... |
| SQL | LIMIT 20 OFFSET 40 | 같음 | WHERE id < 마지막id LIMIT 20 |
| 임의 페이지 이동 | 가능 | 가능 | 불가(다음·이전만) |
| 깊은 페이지 성능 | 느려짐 | 느려짐 | 일정 |
| 데이터가 추가될 때 | 항목이 밀려 중복·누락 | 같음 | 안정적 |
- OFFSET은 앞의 행을 읽고 버려야 해서 뒤 페이지로 갈수록 느려진다
- 커서 방식은 정렬 기준 컬럼(보통 생성 시각 + id)에 인덱스가 있어야 하고, 정렬 기준이 유일해야 경계가 꼬이지 않는다
- 관리 화면처럼 "37페이지로 이동"이 필요하면 오프셋, 무한 스크롤 피드라면 커서가 맞다
Spring Data JPA
Pageable pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending());
Page<Post> page = postRepository.findByAuthor(author, pageable);
page.getContent(); page.getTotalPages(); page.hasNext();Page<T>는 전체 개수를 세는 count 쿼리가 추가로 나간다. 다음 페이지 여부만 필요하면Slice<T>로 바꿔 count를 아낀다- 정렬은 고정(메서드 이름의
OrderBy)과 동적(Sort파라미터) 둘 다 된다(Spring Data 리포지터리와 쿼리 메서드) - 컬렉션을 fetch join하면서 페이지네이션하면 Hibernate가 메모리에서 페이징하므로 피한다(N+1 문제)
DRF
PageNumberPagination,LimitOffsetPagination,CursorPagination세 가지를 제공한다. 전역 설정이나 뷰별pagination_class로 고른다- 제네릭 뷰·ViewSet에서만 자동으로 적용된다(DRF 뷰 계층)
CursorPagination은 정렬 필드(기본-created)가 유일하고 잘 바뀌지 않아야 한다.OrderingFilter와 함께 쓸 때는ordering_fields를 그런 필드로 좁힌다(DRF 필터·검색·정렬)
프론트엔드에서 커서 기반 목록을 이어 붙이는 쪽은 TanStack Query의 무한 쿼리, 긴 목록을 그리는 쪽은 리스트 가상화을 본다.
출처: Use The Index, Luke: No Offset Markus Winand · Spring Data JPA 문서: Paging and Sorting · DRF Pagination: CursorPagination