대규모 데이터를 사용자에게 효과적으로 보여주는 것은 모든 웹/앱 서비스의 중요한 과제입니다. 이때 가장 흔히 사용되는 전략이 바로 페이지네이션(Pagination) 인데요, 대표적으로 Offset 기반과 Cursor(Keyset) 기반 방식이 있습니다.
많은 개발자분들이 Offset 기반 페이지네이션의 편리함 때문에 즐겨 사용하지만, 데이터가 많아지면 치명적인 성능 저하를 겪게 됩니다. 이 글에서는 왜 Offset 방식이 느려지는지 그 내부 메커니즘을 깊이 파헤치고, Cursor 방식이 어떻게 이 문제를 우아하게 해결하는지, 그리고 언제 어떤 방식을 선택해야 하는지에 대한 명확한 가이드라인을 제시합니다.
가장 전통적이고 직관적인 방식입니다. '몇 번째 페이지를 보여줘'라는 요청에 맞춰 LIMIT
(가져올 개수)와 OFFSET
(건너뛸 개수)을 사용합니다.
-- 예시: 3페이지 데이터 요청 (페이지당 10개)
-- 즉, 앞의 20개 항목을 건너뛰고 10개를 가져옵니다.
SELECT *
FROM posts
ORDER BY created_at DESC -- 최신순 정렬
LIMIT 10 -- 10개 항목 가져오기
OFFSET 20; -- 20개 항목 건너뛰기
👍 장점:
page
)만 알면 offset = (page - 1) * limit
공식으로 쉽게 구현 가능합니다.👎 단점:
OFFSET
값이 커질수록) 쿼리 속도가 급격히 느려집니다.🚨 성능 병목 메커니즘: 왜 느려질까?
Offset 기반 페이지네이션의 성능 저하는 바로 'Wasted Work'(낭비되는 작업) 때문입니다.
OFFSET N
은 DB에게 "N개의 행을 건너뛰라"는 명령이지만, DB는 실제로 이 N개의 행을 디스크에서 읽거나 인덱스를 통해 접근하여 처리한 후 버립니다. 즉, OFFSET 20
이면 앞의 20개 행을 처리하는 작업을 수행해야 합니다.OFFSET
값이 10,000, 1,000,000으로 커지면, DB는 수만, 수백만 개의 행을 불필요하게 스캔하고 처리해야 합니다. 이 작업량은 OFFSET
크기에 비례하여 증가하며, I/O 및 CPU 자원을 심각하게 낭비합니다.ORDER BY
컬럼에 인덱스가 있어도, 인덱스를 통해 정렬된 순서로 데이터를 찾는 것은 효율적이지만, 여전히 N개의 레코드를 건너뛰는 작업 자체의 비용은 크게 줄어들지 않습니다.JOIN
이나 복잡한 WHERE
조건, GROUP BY
등이 포함되면 데이터 정렬(Filesort)이나 필터링 비용이 추가되어 OFFSET
처리의 비효율성은 더욱 극대화됩니다.결국, Offset 방식은 사용자에게 보여주지 않을 데이터를 처리하기 위해 막대한 자원을 소모하는 구조적인 한계를 가집니다.
Offset의 단점을 극복하기 위해 등장한 방식으로, 페이지 번호 대신 "마지막으로 본 항목의 값(Cursor)" 을 기준으로 다음 데이터를 조회합니다.
-- 예시: 마지막으로 본 게시물(last_created_at, last_id) 이후의 최신 게시물 10개 조회
SELECT *
FROM posts
WHERE (created_at < :last_created_at) -- 1. 마지막 시간보다 이전 이거나
OR (created_at = :last_created_at AND id < :last_id) -- 2. 시간이 같으면 ID가 더 작은 것
ORDER BY created_at DESC, id DESC -- 정렬 기준은 WHERE절 조건과 일치해야 함
LIMIT 10;
👍 장점:
WHERE
절로 조회 시작점을 명확히 지정하고 인덱스(Range Scan)를 효과적으로 활용하므로, 테이블 크기나 페이지 위치에 거의 영향을 받지 않고 빠른 속도를 유지합니다. 불필요한 데이터 스캔이 없습니다.WHERE
조건과 ORDER BY
절이 인덱스 구조와 잘 맞아떨어져 DB 성능을 최적으로 활용합니다.👎 단점:
WHERE
절이 복잡해집니다.항목 | Offset 기반 페이지네이션 | Cursor(Keyset) 기반 페이지네이션 |
---|---|---|
쿼리 방식 | LIMIT + OFFSET | WHERE <cursor> + LIMIT |
성능 | 페이지 뒤로 갈수록 심각하게 저하 | 테이블 크기/페이지 위치 무관 일관되게 우수 |
데이터 정합성 | 데이터 변경 시 중복/누락 빈번 | 상대적으로 안전 (중복/누락 가능성 낮음) |
페이지 이동 | 특정 페이지 번호로 점프 가능 | 연속 이동(다음/이전)에 특화 (점프 어려움) |
구현 난이도 | 매우 낮음 | 초기 복잡성 높음 (Cursor 관리, 복합 인덱스 등) |
대표 UI | 검색 결과, 관리자 화면 (페이지 번호 노출) | 무한 스크롤, 뉴스피드, 채팅 목록 (연속 로딩) |
✅ Offset 기반을 고려할 수 있는 경우:
🚀 Cursor 기반을 강력히 추천하는 경우:
INDEX(created_at, id)
)🔄 하이브리드 전략:
페이지네이션 전략을 결정하고 구현할 때 다음 사항들을 꼭 확인하세요.
ORDER BY
에 사용될 컬럼에 대한 인덱스가 있는가?WHERE
조건과 ORDER BY
에 사용될 컬럼(들)에 대한 복합 인덱스가 잘 설계되었는가? (순서 중요!)새로운 프로젝트를 시작한다면, 특별한 제약 조건이 없는 한 Cursor 기반 페이지네이션을 기본으로 검토하시기를 강력히 권장합니다. 이미 Offset 기반 시스템에서 성능 문제를 겪고 있다면, Cursor 기반으로의 점진적인 전환을 통해 사용자 경험과 시스템 안정성을 크게 개선할 수 있을 것입니다.