Offset Pagination vs. Cursor Pagination

데이터베이스에서 데이터를 페이지 단위로 가져올 때 사용하는 두 가지 방법, Offset Pagination과 Cursor Pagination에 대해 살펴보고, 각각의 특징과 장단점이 왜 발생하는지 원인을 분석하고 정리해보았다.

1. Pagination 간단히 정리하기

Pagination은 데이터베이스에서 데이터를 한번에 모두 가져오기 힘들 때, 데이터를 잘라서 페이지 형태로 나누어 보여주는 방식을 말한다. 보통 크게 두 가지 방식으로 나눌 수 있다.

  • Offset Pagination
  • Cursor Pagination

아래에서 이 두 가지 방법을 조금 더 자세히 알아본다.

2. Offset Pagination의 개념과 특징

Offset Pagination 개념

Offset Pagination은 원하는 데이터가 전체 데이터에서 몇 번째(offset) 위치에 있는지 지정하고, 그 위치에서부터 몇 개(limit)의 데이터를 가져올지 정하는 방식이다.

예시 SQL 쿼리:

1
SELECT * FROM posts ORDER BY id ASC LIMIT 10 OFFSET 20; -- 3번째 페이지 (페이지당 10개)
1
2
3
4
5
6
const offset = (page - 1) * pageSize;

const [rows] = await db.query(
'SELECT * FROM users ORDER BY id ASC LIMIT ? OFFSET ?',
[pageSize, offset]
);

이 쿼리는 21번째부터 30번째 데이터를 가져온다.

장점과 그 원인

  • 직관적이고 간단한 구현
    • 원인: offset 값만 알면 바로 특정 위치를 찾아서 데이터를 가져올 수 있어서 직관적이다.
  • 페이지 번호로 직접 접근이 쉬움
    • 원인: offset이 페이지 번호를 곱해서 얻어지기 때문에 원하는 페이지로 바로 이동하는 기능을 쉽게 구현할 수 있다.

단점과 그 원인

  • 데이터가 많아질수록 속도가 크게 느려짐
    • 원인: offset이 커질수록 데이터베이스는 앞쪽 데이터를 계속해서 읽고 건너뛰어야 한다. 따라서 데이터가 많아질수록 성능이 떨어진다.
  • 데이터가 추가되거나 삭제되면 중복 또는 누락이 발생할 수 있음
    • 원인: offset은 데이터의 고정된 위치를 기반으로 하기 때문에, 중간에 데이터가 추가되거나 삭제되면 데이터의 위치가 바뀌어 중복 또는 누락이 발생할 수 있다.

데이터 삽입/삭제 시 발생하는 Duplicate/Skipped 문제 사례

실제 데이터 삽입이나 삭제 상황에서 offset pagination이 일으키는 문제는 다음과 같다.

(1) 데이터 삽입으로 인한 Duplicate/Skipped 문제

초기 데이터 예시

Offset ID Name
0 1 A
1 2 B
2 3 C
3 4 D

첫 번째 페이지(offset=0, limit=2)를 조회하면 결과는 [A,B]이다.

이때 새로운 데이터(E)가 최상단(offset=0)에 삽입되면, 데이터는 아래와 같이 바뀐다.

데이터 삽입 후

Offset ID Name
0 5 E(NEW)
1 1 A
2 2 B
3 3 C
4 4 D

두 번째 페이지(offset=2, limit=2)를 조회하면 결과는 [B,C]가 되어 이미 본 데이터(B)가 중복되어 나타나고, 새로 추가된 데이터(E)는 누락(skipped)된다.

왜 중복이 일어나는가?: Offset 방식은 “앞에서 n개를 건너뛴 후(limit, offset)” 데이터를 가져오는데, 중간에 새 데이터가 삽입되면 ‘전체 목록의 순서’ 자체가 바뀐다. 이미 첫 페이지에서 본 데이터가, 두 번째 페이지에 다시 포함될 수도 있다.

(2) 데이터 삭제로 인한 Skipped 문제

초기 데이터 예시

Offset ID Name
0 1 A
1 2 B
2 3 C
3 4 D

첫 번째 페이지(offset=0, limit=2)를 조회하면 [A,B]이다.

이때 데이터 B(ID=2)가 삭제되면, 데이터는 다음과 같이 변경된다.

데이터 삭제 후

Offset ID Name
0 1 A
1 3 C
2 4 D

두 번째 페이지(offset=2, limit=2)를 조회하면 결과는 [D]가 되어, 데이터 C가 누락(skipped)되는 문제가 발생한다.

왜 누락이 일어나는가?: 중간 레코드가 사라져서 전체 기록의 위치(순서)가 당겨지면, OFFSET에서 “이미 본 만큼 건너뛰겠다”라고 계산했을 때 실제로는 건너뛸 행이 줄어들어 버려, 원래 두 번째 페이지에서 보여야 할 데이터가 건너뛰기에 포함될 수 있다.


3. Cursor Pagination의 개념과 특징

Cursor Pagination 개념

Cursor Pagination은 이전 페이지 마지막으로 가져온 데이터의 특정 키(id 또는 timestamp 등)를 기준으로, 그 뒤에 있는 데이터를 가져오는 방식이다.

예시 SQL 쿼리:

1
2
-- 이전 페이지의 마지막 id가 100일 때, 다음 10개 데이터를 가져옴
SELECT * FROM posts WHERE id > 100 ORDER BY id ASC LIMIT 10;
1
2
3
4
const [rows] = await db.query(
'SELECT * FROM users WHERE id > ? ORDER BY id ASC LIMIT ?',
[cursor, pageSize]
);

이 쿼리는 id가 100 이후의 데이터를 가져오게 된다.

장점과 그 원인

  • 데이터가 많아져도 성능이 거의 변하지 않음
    • 원인: 특정 키(cursor)를 기준으로 데이터를 바로 가져오기 때문에, 데이터를 처음부터 스캔할 필요가 없어서 성능 저하가 거의 없다.
  • 데이터가 실시간으로 추가/삭제되더라도 중복이나 누락이 없음
    • 원인: 명확한 키 값을 기준(cursor)으로 데이터를 가져오기 때문에 중간에 데이터가 변하더라도 데이터를 놓치거나 중복으로 가져오는 문제가 발생하지 않는다.

단점과 그 원인

  • 특정 페이지로 바로 이동하는 것이 어려움
    • 원인: cursor를 기반으로 움직이기 때문에 중간 페이지로 바로 이동하려면, 그 페이지까지 cursor를 계속 따라가야 한다. 페이지 번호를 바로 찾아가는 것이 어렵다.
  • 커서(cursor)가 될 수 있는 키 값이 반드시 유일하고 정렬 가능한 값이어야 함
    • 원인: cursor가 명확히 정렬되지 않거나 중복이 있으면 데이터를 제대로 가져올 수 없기 때문에 안정적인 키 값을 반드시 사용해야 한다.

4. Offset Pagination과 Cursor Pagination 비교 정리

항목 Offset Pagination Cursor Pagination
성능 데이터 양에 따라 성능 저하 심각 데이터 양에 상관없이 일정한 성능
데이터 일관성 데이터 추가/삭제에 취약(중복/누락) 데이터 추가/삭제 시에도 안정적
임의 접근 가능성 페이지 번호로 직접 접근 가능 페이지 번호로 바로 접근 어려움
구현 난이도 간단하고 쉬움 약간 복잡하며 키 값 선정이 중요함

5. 각 Pagination 방식의 적합한 상황 정리

Offset Pagination이 더 좋은 상황

  • 게시판, 쇼핑몰처럼 페이지 번호로 바로 이동해야 하는 서비스
  • 데이터 양이 많지 않고, 실시간 변동이 적어 성능 문제가 크지 않은 경우

Cursor Pagination이 더 좋은 상황

  • 소셜 미디어, 채팅 등 데이터가 실시간으로 계속 추가/삭제되는 서비스
  • 무한 스크롤 방식을 사용하는 서비스로 성능이 매우 중요할 때
  • 데이터 양이 많아서 offset 방식으로 성능을 유지하기 어려운 경우

6. 결론과 권장 사항

Offset Pagination은 간단하고 직관적이지만, 데이터가 많아지면 성능이 떨어지고 데이터 일관성이 문제될 수 있다. 반면 Cursor Pagination은 데이터가 많아져도 성능과 일관성을 유지할 수 있지만, 페이지 번호로의 직접 접근이 어렵고, 커서(cursor)를 신중히 선택해야 하는 단점이 있다.

따라서 Pagination 방식을 선택할 때는 데이터의 양, 변동성, 그리고 사용자가 원하는 페이지 접근 방식 등을 고려하여 적절한 방법을 결정하는 것이 중요하다.

정리하자면, 일반적인 웹사이트라면 Offset Pagination을, 데이터가 많고 실시간 변동이 많은 서비스라면 Cursor Pagination을 사용하는 것이 더 좋다.