OFFSET 페이지네이션을 커서 기반 조회로 개선하기

dddingzong·2026년 4월 17일

이론

목록 보기
13/14

서비스에서 게시글, 주문 내역, 알림 목록처럼 여러 데이터를 페이지 단위로 조회하는 기능은 자주 사용된다.

처음에는 LIMITOFFSET을 자주 사용한다. 그러나 데이터가 많아질수록 이 방식은 점점 느려질 수 있다.

이번 글에서는 OFFSET 기반 페이지네이션이 왜 비효율적일 수 있는지 확인하고, 이를 커서 기반 페이지네이션으로 어떻게 개선할 수 있는지 정리해보자.어떤 인덱스 전략이 필요한지와 서비스 설계 관점에서 무엇을 고려해야 하는지도 함께 살펴보자.

1. LIMIT / OFFSET

가장 흔한 페이지네이션 쿼리는 다음과 같다.

SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 0;

첫 페이지를 조회할 때는 문제가 없어 보인다. 두 번째 페이지는 OFFSET 20, 세 번째 페이지는 OFFSET 40처럼 증가시키면 된다.

SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 100000;

문제는 페이지가 뒤로 갈수록 발생한다. 앞부분의 데이터를 실제로 반환하지는 않더라도, 데이터베이스는 그만큼의 행을 건너뛰는 비용을 감당해야 한다. 즉, 조회 대상이 뒤로 밀릴수록 성능이 점점 악화될 수 있다.

2. OFFSET 방식이 느려지는 이유

OFFSET은 말 그대로 앞의 N개를 건너뛴 뒤, 그 다음 데이터부터 가져오라는 의미다. 겉으로 보기에는 단순하지만, 내부적으로는 건너뛸 대상도 어느 정도는 확인해야 한다.

예를 들어 아래 쿼리를 보자.

SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 100000;

이 쿼리는 “100001번째 데이터부터 20개만 가져오면 되겠네”처럼 보이지만, 실제로는 그 이전 행들에 대한 정렬 순서를 맞추고, 필요한 위치까지 이동해야 한다. 결국 OFFSET 값이 커질수록 불필요하게 읽는 범위도 함께 커진다.

이 방식은 특히 다음과 같은 경우 더 불리하다.

  • 데이터가 매우 많은 경우
  • 사용자가 뒤 페이지까지 자주 탐색하는 경우
  • 정렬 기준이 명확하고 최신순 조회가 많은 경우
  • 무한 스크롤처럼 연속 조회가 자주 발생하는 경우

즉, 작은 규모에서는 단순한 구현이 장점이지만, 대용량 조회에서는 한계가 분명하다.

3. 커서 기반 페이지네이션이란 무엇인가

이 문제를 줄이기 위해 자주 사용하는 방식이 커서 기반 페이지네이션이다. 핵심은 “몇 번째 페이지인가”를 기준으로 조회하지 않고, “이전 페이지의 마지막 데이터 이후부터 가져와라”라는 방식으로 바꾸는 것이다.

예를 들어 첫 페이지는 그대로 최신 데이터 20개를 가져온다.

SELECT id, title, created_at
FROM posts
ORDER BY created_at DESC, id DESC
LIMIT 20;

그리고 응답으로 받은 마지막 행의 created_at, id 값을 다음 요청의 기준점으로 사용한다. 다음 페이지는 아래처럼 조회할 수 있다.

SELECT id, title, created_at
FROM posts
WHERE (created_at < '2026-04-17 10:30:00')
   OR (created_at = '2026-04-17 10:30:00' AND id < 1050)
ORDER BY created_at DESC, id DESC
LIMIT 20;

이 방식은 “앞의 100000개를 건너뛰고”가 아니라 “이 기준보다 뒤에 있는 데이터만 가져와라”로 바뀐다. 즉, 데이터베이스가 필요한 범위부터 바로 찾을 수 있게 된다.

4. 왜 created_at만 쓰지 않고 id까지 같이 써야 할까

커서 기반 페이지네이션을 설명할 때 created_at 하나만 기준으로 두는 예시가 많다. 하지만 실제 서비스에서는 같은 시각에 여러 데이터가 생성될 수 있다. 이 경우 created_at만으로는 정렬 순서가 완전히 고정되지 않는다.

예를 들어 아래 두 데이터가 있다고 하자.

idcreated_at
10502026-04-17 10:30:00
10492026-04-17 10:30:00

둘의 생성 시각이 같다면 created_at DESC만으로는 어떤 것이 먼저인지 보장하기 어렵다. 이 상태에서 커서를 created_at 하나만 사용하면 다음 페이지에서 일부 데이터가 중복되거나 누락될 수 있다.

그래서 정렬 기준은 보통 다음처럼 잡는다.

ORDER BY created_at DESC, id DESC

그리고 커서 조건도 동일하게 맞춘다.

WHERE (created_at < :lastCreatedAt)
   OR (created_at = :lastCreatedAt AND id < :lastId)

즉, 정렬 기준과 커서 조건은 반드시 일관되어야 한다. 이 부분이 빠지면 커서 기반 페이지네이션은 겉보기만 안정적일 뿐, 실제로는 데이터 정합성 문제가 생길 수 있다.

5. 인덱스 전략: 쿼리만 바꿔서는 충분하지 않다

커서 기반 페이지네이션이 효과를 보려면 인덱스도 함께 설계해야 한다. 다음과 같은 조회가 많다고 가정해보자.

SELECT id, title, created_at
FROM posts
WHERE status = 'PUBLIC'
  AND (created_at < :lastCreatedAt
       OR (created_at = :lastCreatedAt AND id < :lastId))
ORDER BY created_at DESC, id DESC
LIMIT 20;

이 경우 자주 사용하는 조건과 정렬 기준을 반영한 인덱스를 고려해야 한다. 예를 들면 다음과 같다.

INDEX idx_posts_status_created_id (status, created_at DESC, id DESC)

해당 인덱스가 필요한 이유는

첫째, status = 'PUBLIC' 같은 필터 조건을 빠르게 줄일 수 있어야 한다. 둘째, 그 상태에서 created_at, id 순으로 이미 정렬된 자료를 따라가며 필요한 범위만 읽을 수 있어야 한다. 셋째, LIMIT 20처럼 적은 양만 가져올 때 특히 효율이 높아진다.

여기서 중요한 점은 인덱스 컬럼 순서다. 인덱스는 아무 컬럼이나 많이 넣는다고 좋아지는 것이 아니라, 실제 조회 패턴에 맞아야 한다. 즉, “어떤 WHERE 조건을 자주 쓰는가”, “어떤 ORDER BY가 붙는가”, “LIMIT 조회가 많은가”를 먼저 보고 설계해야 한다.

6. OFFSET 방식과 커서 방식 비교

OFFSET 방식의 장점

  • 구현이 단순하다
  • 페이지 번호 기반 UI와 잘 맞는다
  • 1페이지, 2페이지, 10페이지처럼 직접 이동하기 쉽다

OFFSET 방식의 단점

  • 뒤 페이지로 갈수록 성능이 떨어질 수 있다
  • 대용량 데이터에서 비효율적이다
  • 무한 스크롤과는 잘 맞지 않는다

커서 방식의 장점

  • 대용량 조회에서 더 효율적이다
  • 무한 스크롤 방식과 잘 맞는다
  • 필요한 범위부터 바로 읽기 쉬워 성능상 유리하다

커서 방식의 단점

  • 구현이 더 복잡하다
  • 페이지 번호 기반 이동과는 잘 맞지 않는다
  • 정렬 기준과 커서 조건을 정확히 맞춰야 한다

결국 어느 방식이 무조건 정답이라고 보기는 어렵다. 다만 최신순 목록을 연속으로 조회하는 서비스라면 커서 방식이 더 적합한 경우가 많다.

7. SQL 리팩토링 관점에서 무엇이 바뀐 것인가

기존 방식은 이런 생각에 가깝다.

  • 전체 정렬 결과가 있다
  • 그중 앞의 N개를 건너뛴다
  • 다음 M개를 가져온다

반면 커서 방식은 이렇게 바뀐다.

  • 마지막으로 본 데이터의 위치를 기억한다
  • 그 이후 범위만 조회한다
  • 필요한 데이터 수만큼만 가져온다

즉, 전체 결과 집합을 기준으로 일부를 잘라오는 방식에서 현재 위치를 기준으로 다음 구간만 조회하는 방식으로 사고가 바뀐 것이다.

이 차이는 단순히 성능 개선뿐 아니라, 서비스 API를 설계하는 방식에도 영향을 준다.

8. 아키텍처 관점에서 고려할 점

페이지네이션은 SQL 한 줄의 문제가 아니라 API 설계 문제이기도 하다.

예를 들어 관리자 페이지처럼 “정확히 37페이지 중 12페이지”로 이동해야 하는 화면이라면 OFFSET이 더 적합할 수 있다. 반대로 SNS 피드, 알림 목록, 주문 내역처럼 연속적인 조회 구조라면 커서 방식이 더 자연스럽다.

또한 커서 방식은 보통 응답 값도 달라진다. 기존에는 page, size, totalPages 같은 정보가 중심이었다면, 커서 방식에서는 아래와 같은 구조가 더 일반적이다.

{
  "items": [...],
  "nextCursor": {
    "createdAt": "2026-04-17T10:30:00",
    "id": 1050
  },
  "hasNext": true
}

즉, API 사용자도 “다음 페이지 번호”가 아니라 “다음 조회 기준점”을 받아야 한다. 따라서 페이지네이션 전략은 프론트엔드와 백엔드가 함께 합의해야 하는 부분이다.

9. 실제로는 언제 바꿔야 할까

모든 조회를 처음부터 커서 기반으로 만들 필요는 없다. 작은 규모의 관리자 페이지나 검색 결과처럼 직접 페이지 이동이 중요한 경우에는 OFFSET이 더 편할 수 있다.

다만 아래 조건에 해당하면 커서 기반 페이지네이션을 우선 검토할 필요가 있다.

  • 데이터가 빠르게 누적되는 목록이다
  • 최신순 정렬이 기본이다
  • 무한 스크롤 UI를 사용한다
  • 응답 속도가 중요하다
  • 뒤 페이지 조회가 빈번하다

10. 마무리

처음에는 LIMITOFFSET이 가장 단순하고 익숙한 해결책처럼 보인다. 하지만 데이터가 많아지고 조회가 반복될수록, 이 방식은 점점 비효율적일 수 있다. 특히 최신순 목록을 계속 조회하는 서비스에서는 앞부분을 반복해서 건너뛰는 비용이 누적되기 쉽다.

커서 기반 페이지네이션은 이 문제를 줄일 수 있는 현실적인 대안이다. 다만 쿼리만 바꾸는 것으로 끝나지 않는다. 정렬 기준을 안정적으로 설계해야 하고, 그에 맞는 복합 인덱스를 준비해야 하며, API 응답 구조와 프론트엔드 동작 방식도 함께 고려해야 한다.

결국 쿼리 최적화는 SQL 문법 몇 줄의 문제가 아니다. 어떤 데이터를 어떤 방식으로 보여줄 것인지, 그리고 그 조회 패턴에 맞는 인덱스와 API를 어떻게 설계할 것인지까지 함께 보는 것이 중요하다. 이번 사례는 그 점을 잘 보여주는 대표적인 예시라고 생각한다.

profile
공부 노트 & 회고

0개의 댓글