Offset vs. Cursor(Keyset) 페이지네이션 완벽 분석: 성능 병목의 원인과 해결책

이동휘·2025년 5월 4일
0

매일매일 블로그

목록 보기
5/49

대규모 데이터를 사용자에게 효과적으로 보여주는 것은 모든 웹/앱 서비스의 중요한 과제입니다. 이때 가장 흔히 사용되는 전략이 바로 페이지네이션(Pagination) 인데요, 대표적으로 Offset 기반Cursor(Keyset) 기반 방식이 있습니다.

많은 개발자분들이 Offset 기반 페이지네이션의 편리함 때문에 즐겨 사용하지만, 데이터가 많아지면 치명적인 성능 저하를 겪게 됩니다. 이 글에서는 왜 Offset 방식이 느려지는지 그 내부 메커니즘을 깊이 파헤치고, Cursor 방식이 어떻게 이 문제를 우아하게 해결하는지, 그리고 언제 어떤 방식을 선택해야 하는지에 대한 명확한 가이드라인을 제시합니다.


1. Offset 기반 페이지네이션: 편리함 속의 성능 함정

가장 전통적이고 직관적인 방식입니다. '몇 번째 페이지를 보여줘'라는 요청에 맞춰 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 공식으로 쉽게 구현 가능합니다.
  • 자유로운 페이지 이동: 사용자가 원하는 특정 페이지 번호(예: 1페이지 → 5페이지)로 즉시 이동하는 UI 구현이 쉽습니다.

👎 단점:

  • 성능 저하 (치명적): 페이지 번호가 뒤로 갈수록(OFFSET 값이 커질수록) 쿼리 속도가 급격히 느려집니다.
  • 데이터 정합성 문제: 페이지 조회 중 데이터가 삽입/삭제되면, 다음 페이지에서 데이터가 중복되거나 누락될 수 있습니다.

🚨 성능 병목 메커니즘: 왜 느려질까?

Offset 기반 페이지네이션의 성능 저하는 바로 'Wasted Work'(낭비되는 작업) 때문입니다.

  1. 불필요한 데이터 스캔: OFFSET N은 DB에게 "N개의 행을 건너뛰라"는 명령이지만, DB는 실제로 이 N개의 행을 디스크에서 읽거나 인덱스를 통해 접근하여 처리한 후 버립니다. 즉, OFFSET 20이면 앞의 20개 행을 처리하는 작업을 수행해야 합니다.
  2. OFFSET 증가 = 비용 선형 증가: OFFSET 값이 10,000, 1,000,000으로 커지면, DB는 수만, 수백만 개의 행을 불필요하게 스캔하고 처리해야 합니다. 이 작업량은 OFFSET 크기에 비례하여 증가하며, I/O 및 CPU 자원을 심각하게 낭비합니다.
  3. 인덱스의 한계: ORDER BY 컬럼에 인덱스가 있어도, 인덱스를 통해 정렬된 순서로 데이터를 찾는 것은 효율적이지만, 여전히 N개의 레코드를 건너뛰는 작업 자체의 비용은 크게 줄어들지 않습니다.
  4. 복잡한 쿼리에서 악화: JOIN이나 복잡한 WHERE 조건, GROUP BY 등이 포함되면 데이터 정렬(Filesort)이나 필터링 비용이 추가되어 OFFSET 처리의 비효율성은 더욱 극대화됩니다.

결국, Offset 방식은 사용자에게 보여주지 않을 데이터를 처리하기 위해 막대한 자원을 소모하는 구조적인 한계를 가집니다.


2. Cursor(Keyset) 기반 페이지네이션: 성능과 정합성을 잡는 현대적 해법

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 성능을 최적으로 활용합니다.

👎 단점:

  • 페이지 점프 어려움: 특정 페이지 번호로 바로 이동하는 기능 구현이 복잡하거나 불가능합니다. 주로 "더 보기", "무한 스크롤" UI에 적합합니다.
  • 구현 복잡성 증가: 클라이언트와 서버가 마지막 항목의 Cursor 값을 상태로 관리하고 주고받아야 합니다. 복합 정렬 기준을 사용하면 WHERE 절이 복잡해집니다.
  • 정렬 기준 제한: Cursor로 사용할 컬럼(들)은 고유하거나 고유성을 보장할 수 있는 조합이어야 하며, 해당 컬럼(들) 기반의 정렬에 최적화됩니다.

3. 핵심 비교: Offset vs. Cursor 한눈에 보기

항목Offset 기반 페이지네이션Cursor(Keyset) 기반 페이지네이션
쿼리 방식LIMIT + OFFSETWHERE <cursor> + LIMIT
성능페이지 뒤로 갈수록 심각하게 저하테이블 크기/페이지 위치 무관 일관되게 우수
데이터 정합성데이터 변경 시 중복/누락 빈번상대적으로 안전 (중복/누락 가능성 낮음)
페이지 이동특정 페이지 번호로 점프 가능연속 이동(다음/이전)에 특화 (점프 어려움)
구현 난이도매우 낮음초기 복잡성 높음 (Cursor 관리, 복합 인덱스 등)
대표 UI검색 결과, 관리자 화면 (페이지 번호 노출)무한 스크롤, 뉴스피드, 채팅 목록 (연속 로딩)

4. 선택 가이드: 언제 무엇을 써야 할까?

✅ Offset 기반을 고려할 수 있는 경우:

  • 페이지 번호 UI가 필수적일 때 (예: 검색 결과 페이지, 관리자용 데이터 테이블).
  • 데이터 규모가 작고, 실시간 변경이 거의 없을 때.
  • 성능 저하를 감수하거나, 조회 가능한 최대 페이지 수를 제한할 수 있을 때 (예: "10페이지까지만 제공").

🚀 Cursor 기반을 강력히 추천하는 경우:

  • SNS 피드, 채팅 목록, 로그 데이터 등 데이터 양이 많고 실시간 변경이 잦은 서비스.
  • 성능과 데이터 정합성이 매우 중요할 때.
  • 무한 스크롤이나 "더 보기" 방식의 UI/UX를 채택할 때.
  • 복합 정렬이 필요하다면, 해당 컬럼들에 대한 복합 인덱스 설계가 필수입니다. (예: INDEX(created_at, id))

🔄 하이브리드 전략:

  • 첫 N 페이지(예: 1~5페이지)는 Offset으로 제공하여 페이지 이동 편의성을 주고, 그 이후는 Cursor로 전환하여 성능을 보장하는 방식.
  • 내부 관리자 도구는 Offset, 외부 사용자 서비스는 Cursor로 이원화하여 운영하는 방식.

5. 실무 적용 체크리스트 ✔️

페이지네이션 전략을 결정하고 구현할 때 다음 사항들을 꼭 확인하세요.

  1. UI/UX 요구사항: 페이지 번호로 점프해야 하는가, 아니면 더보기/무한 스크롤인가?
  2. 데이터 특성: 예상 데이터 최대 크기는? 실시간 삽입/삭제 빈도는?
  3. 인덱스 설계:
    • Offset: ORDER BY에 사용될 컬럼에 대한 인덱스가 있는가?
    • Cursor: WHERE 조건과 ORDER BY에 사용될 컬럼(들)에 대한 복합 인덱스가 잘 설계되었는가? (순서 중요!)
  4. API 설계: Cursor 값은 어떻게 클라이언트와 주고받을 것인가? (보통 다음 페이지 요청 시 이전 페이지 마지막 항목의 값 전달. URL-safe 인코딩(예: Base64) 고려) API 멱등성 보장 방안은?
  5. 테스트: 데이터 삽입/삭제가 동시에 발생하는 시나리오에서 페이지 중복/누락 문제가 없는지 충분히 테스트했는가?

마무리

  • Offset: 구현은 쉽지만, 데이터가 많아지면 피할 수 없는 성능 저하정합성 문제를 안고 있는 전통적인 방식입니다.
  • Cursor: 초기 구현과 설계가 더 복잡하지만, 대규모 데이터에서도 항상 빠르고 안정적인 성능높은 데이터 정합성을 제공하는 현대적인 표준 방식입니다.

새로운 프로젝트를 시작한다면, 특별한 제약 조건이 없는 한 Cursor 기반 페이지네이션을 기본으로 검토하시기를 강력히 권장합니다. 이미 Offset 기반 시스템에서 성능 문제를 겪고 있다면, Cursor 기반으로의 점진적인 전환을 통해 사용자 경험과 시스템 안정성을 크게 개선할 수 있을 것입니다.


0개의 댓글