안녕하세요, 백엔드 개발자입니다.
지난 편에서는 OFFSET을 사용한 페이지네이션이 데이터 규모가 커질수록 왜 성능 문제를 일으킬 수 있는지 알아보았습니다. 불필요한 데이터를 대량으로 읽고 버리는 내부 동작 방식 때문이었죠.
이번 글에서는 이에 대한 해결책으로, 대규모 서비스에서 안정적인 성능을 위해 사용하는 '커서 기반 페이지네이션(Cursor-based Pagination)' 기법을 소개하겠습니다.
1. 접근 방식의 전환: 페이지 번호에서 데이터 기준으로
커서 기반 페이지네이션의 핵심 아이디어는 '페이지 번호' 대신 '특정 데이터의 위치'를 기준으로 다음 목록을 조회하는 것입니다.
- OFFSET 기반: "3번째 페이지의 목록을 줘"
- 커서 기반: "지난번 마지막으로 본 게시글 ID 12345 다음 목록을 줘"
여기서 '게시글 ID 12345'와 같이 기준점이 되는 값을 커서(Cursor) 라고 부릅니다. 커서는 보통 정렬의 기준이 되는, 중복되지 않는 유니크한 컬럼(대부분 id나 created_at)을 사용합니다.
2. 커서 기반 페이지네이션 구현
id를 기준으로 내림차순 정렬하는 목록 API를 예로 들어보겠습니다.
OFFSET 기반 방식
-- 2페이지 조회 (OFFSET 10)
SELECT * FROM posts ORDER BY id DESC LIMIT 10 OFFSET 10;
커서 기반 방식
-- 1. 첫 페이지 조회 (커서 없음)
SELECT * FROM posts ORDER BY id DESC LIMIT 10;
첫 페이지를 조회한 결과, 마지막 게시글의 id가 991이라고 가정해 보겠습니다. 클라이언트는 이 991 값을 '다음 페이지를 요청하기 위한 커서'로 가지고 있습니다.
-- 2. 다음 페이지 조회 (클라이언트가 마지막 id '991'을 커서로 전달)
SELECT *
FROM posts
WHERE id < 991 -- 마지막으로 본 id보다 작은 데이터를 조회
ORDER BY id DESC
LIMIT 10;
이처럼 OFFSET 대신 WHERE 조건절을 사용하여 조회 시작점을 명확하게 지정하는 것이 핵심입니다.
3. 인덱스를 활용한 성능 개선 원리
커서 기반 방식이 OFFSET보다 빠른 이유는 **인덱스(Index)**를 효율적으로 활용하기 때문입니다.
OFFSET 쿼리는 ORDER BY를 위해 많은 데이터를 읽어야 하므로 인덱스의 이점을 온전히 활용하기 어렵습니다.
반면 커서 기반 쿼리의 WHERE id < 991 조건은 다릅니다. 데이터베이스는 id 컬럼의 인덱스를 사용해 id가 991인 지점을 빠르게 찾은 뒤, 그 지점부터 순서대로 10개의 데이터만 읽으면 됩니다. 조회하려는 페이지가 아무리 뒤쪽에 있어도, DB가 수행하는 작업은 '특정 지점을 찾아 그 뒤부터 N개 읽기'로 동일합니다. 이 덕분에 일관된 성능을 유지할 수 있습니다.
4. 장점과 한계
커서 기반 페이지네이션은 강력하지만, 모든 상황에 적합한 것은 아닙니다.
- 장점
- 일관된 성능: 페이지가 깊어져도 쿼리 속도가 거의 일정하게 유지됩니다.
- 데이터 정확성: 조회 도중 데이터가 추가/삭제되어도 중복이나 누락이 발생할 가능성이 적습니다.
- 한계
- 페이지 점프 불가: "50페이지로 바로가기"와 같은 기능을 구현할 수 없습니다. '다음' 또는 '이전' 목록만 순차적으로 가져올 수 있습니다.
- 구현 복잡도: 클라이언트가 마지막 커서 값을 관리해야 하는 등, OFFSET 방식보다 구현이 조금 더 복잡합니다.
이러한 특성 때문에, 커서 기반 페이지네이션은 주로 무한 스크롤(Infinite Scroll) 이나 더 보기(Load More) 형태의 UI에 가장 적합합니다.
5. 정리하며
지금까지 OFFSET의 대안으로 커서 기반 페이지네이션을 알아보았습니다.
대용량 데이터를 다루는 서비스에서 안정적인 성능을 확보하려면 커서 기반 페이지네이션은 매우 효과적인 해결책이 될 수 있습니다. 만약 구현하려는 기능이 무한 스크롤 방식이라면, 처음부터 이 방식을 도입하는 것을 적극적으로 고려해 보시길 바랍니다.
긴 글 읽어주셔서 감사합니다.
'Backend' 카테고리의 다른 글
| COUNT(*) 대신 EXISTS 써보기 (0) | 2025.09.08 |
|---|---|
| 목록 조회 API, OFFSET 페이지네이션 괜찮을까? (0) | 2025.09.01 |
| 나만의 데코레이터 만들기: 실전 기능 구현 (0) | 2025.08.29 |
| 나만의 데코레이터 만들기: NestJS에서 반복적인 API 설정 줄이기 (0) | 2025.08.27 |
| 서버 부하를 줄이는 이미지 업로드 전략 (3편): 유연한 썸네일 URL 관리 패턴 (0) | 2025.08.24 |
