서비스 개발을 하다 보면 대부분의 목록 조회 API에서 페이징(Pagination) 이 필요합니다.
단순히 “페이지 나누기”로만 보면 간단해 보이지만, 데이터 양이나 사용자 경험(UX) 에 따라 선택해야 할 페이징 방식이 달라집니다.
이번 글에서는 대표적인 페이징 기법들을 정리하고, 각각 간단한 예제를 통해 차이를 알아보겠습니다.
1. 단순 페이징 (Simple Pagination)
개념
가장 직관적인 방식입니다.
데이터를 전부 가져온 다음, 메모리에서 잘라서 보여주는 방식이죠.
// 예시: 전체 리스트에서 subList로 잘라내기
List<String> list = fetchAllData(); // DB에서 모든 데이터 불러옴
List<String> page = list.subList(0, 10); // 첫 10개만 사용
특징
- 구현이 아주 간단합니다.
- 하지만 DB에서 전체 데이터를 가져오기 때문에 비효율적입니다.
언제 쓰나?
- 데이터가 아주 적을 때 (수십~수백 건).
- 캐시된 데이터나 테스트용 코드에서만.
2. OFFSET 기반 페이징 (JPA 기본 방식)
개념
SQL의 LIMIT … OFFSET … 구문을 이용하는 방식입니다.
JPA에서 Pageable을 쓰면 바로 이 방식이 적용됩니다.
SELECT *
FROM posts
ORDER BY created_at
LIMIT 10 OFFSET 20; -- 3번째 페이지
JPA 코드 예시:
Page<Post> page = postRepository.findAll(
PageRequest.of(2, 10, Sort.by("createdAt").descending())
);
특징
- 장점:
- “n번째 페이지”로 바로 점프가 가능합니다.
- JPA 기본 기능이라 구현이 쉽습니다.
- 단점:
- OFFSET이 커질수록 성능 저하.
(예: OFFSET 100000 → DB가 100,000개 스캔 후 버리고 뒤의 10개만 돌려줌)
- OFFSET이 커질수록 성능 저하.
언제 쓰나?
- 데이터 양이 적을 때 (수만 건 정도).
- 관리자 페이지처럼 “임의의 페이지 이동”이 중요한 경우.
3. Keyset 페이징 (인덱스 기반, Seek 방식)
개념
OFFSET 대신 인덱스를 기준으로 다음 데이터를 찾는 방식입니다.
보통 WHERE 조건으로 “마지막으로 본 값”을 넘겨줍니다.
SELECT *
FROM posts
WHERE created_at > '2025-09-19 12:00:00'
ORDER BY created_at
LIMIT 10;
JPA (네이티브 쿼리) 예시:
@Query(value = "SELECT * FROM posts " +
"WHERE created_at > :lastCreatedAt " +
"ORDER BY created_at LIMIT :limit",
nativeQuery = true)
List<Post> findNextPage(@Param("lastCreatedAt") Instant lastCreatedAt,
@Param("limit") int limit);
특징
- 장점:
- OFFSET 스킵이 없어 성능이 일정하고 빠릅니다.
- 대규모 데이터(수백만 건 이상)에도 적합.
- 단점:
- “100번째 페이지로 점프” 같은 기능은 구현이 어렵습니다.
언제 쓰나?
- 무한 스크롤, API 응답, SNS 피드처럼 순차 탐색이 필요한 경우.
- 데이터가 매우 많을 때.
4. Hybrid 방식 (OFFSET + Keyset 혼합)
개념
처음에는 OFFSET으로 진입하고, 이후 스크롤은 Keyset 방식으로 이어가는 방법입니다.
예를 들어, 사용자가 100번째 페이지로 점프하면 OFFSET을 쓰고, 이후 스크롤은 Keyset으로 불러오는 형태입니다.
특징
- 장점: 임의 페이지 점프 + 연속 스크롤 효율 모두 잡을 수 있음.
- 단점: 구현 난이도가 조금 높음.
언제 쓰나?
- 관리자 페이지와 사용자 피드가 섞여 있는 서비스.
- 특정 위치에서 진입 후, 스크롤 탐색이 이어지는 경우.
5. Cursor 기반 페이징 (API 친화적 Keyset)
개념
Keyset 방식의 확장판으로, 커서(cursor) 라는 토큰을 클라이언트에 전달하고 다시 요청 시 사용합니다.
GET /posts?cursor=eyJpZCI6MTAwLCJjcmVhdGVkX2F0IjoiMjAyNS0wOS0xOVQxMjowMDowMCJ9
서버는 이 커서를 디코딩해서 WHERE created_at > ... 조건으로 조회합니다.
특징
- 장점:
- REST/GraphQL API에 적합.
- 클라이언트는 커서만 알면 “다음 페이지” 요청 가능.
- 단점:
- 커서 인코딩/디코딩 로직 필요.
- 임의 페이지 점프는 불가.
언제 쓰나?
- 페이스북, 트위터, 인스타그램 같은 대규모 서비스 API.
- 모바일 앱 무한 스크롤.
페이징 기법 비교
| 방식 | 장점 | 단점 | 적합한 사례 |
| 단순 페이징 | 구현 가장 쉬움 | 전체 데이터 불러오기 → 비효율 | 소규모 데이터 |
| OFFSET (JPA) | n번째 페이지 점프 가능, 기본 기능 | OFFSET 커지면 성능 저하 | 관리자 페이지 |
| Keyset | 대용량에서도 일정한 성능 | 임의 페이지 점프 불가 | 무한 스크롤, API |
| Hybrid | 점프+효율 둘 다 확보 | 구현 복잡 | 혼합형 서비스 |
| Cursor | API 친화적, 안정성 | 커서 관리 필요 | 대형 서비스 API |
정리
- 작은 데이터셋: OFFSET 기반(JPA 기본 페이징)으로 충분
- 대규모 데이터 / 무한 스크롤: Keyset 또는 Cursor 기반 필수
- 특수한 경우(점프+스크롤): Hybrid 고려
페이징은 단순히 데이터를 나누는 게 아니라, 서비스의 UX와 성능을 동시에 잡는 중요한 설계 포인트
'프로젝트' 카테고리의 다른 글
| Prometheus와 Grafana 이해하기 (0) | 2025.11.06 |
|---|---|
| 트랜잭션(Transaction) 이해 (0) | 2025.09.24 |
| 테스트 커버리지 점진적으로 높이기 (1) | 2025.08.25 |
| JaCoCo + Codecov로 CI 테스트 커버리지 관리하기 (4) | 2025.08.25 |
| 로깅(Logging)과 Logback 정리 (3) | 2025.08.09 |