[Spring]오프셋 페이징보다 커서 페이징을 써야하는 이유

2025. 4. 25. 10:42·Spring
💡 커서 기반 페이징이 가장 효율적인 방법이며, 가능한 항상 사용되어야 한다
-페이스북 개발자

 

이번 글에서는 커서 페이징이 무엇이고, 왜 요즘처럼 실시간 데이터가 중요한 시대에 커서 페이징이 각광받는지 살펴본다. 또한 오프셋 페이징과의 차이점, 커서 페이징의 장단점, 그리고 언제 어떤 방식을 선택해야 하는지에 대해서도 함께 이야기해보자.

✅ 페이징이란?

  • 페이징은 책 페이지처럼 데이터를 묶음으로 분리하는 과정이다.

 

페이징은 총 2개의 종류로 나뉘는데 제목처럼 오프셋 페이징과 커서 페이징 두개로 나뉜다.

 

 

♐ 오프셋 페이징

  • OFFSET 값을 포함한 SQL 쿼리문이 동반되며 데스크탑 웹에서 주로 사용하는 페이징 기법이다.
  • 명확한 페이지 버튼이 있어 클릭 시에 해당 페이지로 넘어갈 수 있다.

Photo by  Creative Market

♐ 커서 페이징

  • 사용자가 대량의 데이터를 사용하는 웹이나 앱 어플리케이션에서 사용하는 페이징 기법이다.
  • 하단으로 계속 스크롤시에 자동으로 페이징이 지원된다.

 


✅ 커서 페이징을 사용해야하는 이유 

1️⃣ 실시간 데이터 안정성

오프셋 기반 페이징

SELECT * FROM posts ORDER BY created_at DESC OFFSET 10 LIMIT 5;

 

오프셋 페이징의 문제는 이런 방식이 데이터를 정적인 상태로 간주한다는 점이다.

새로운 데이터가 삽입되거나 삭제될 경우, 중복되거나 누락된 데이터가 사용자에게 노출될 수 있다.

 

예를 들어, 사용자가 페이지 1을 조회한 후 Sam이 실시간으로 사진을 삭제하면, 페이지 2에서 우리가 기대했던 고양이 사진이 사라질 수 있다. 또는 새 데이터가 추가되면서 같은 데이터를 두 번 보게 될 수도 있다.

 

❗ 이유: OFFSET 페이징은 데이터의 "위치"를 기준으로 페이지를 나누기 때문이다.

 

 OFFSET 방식은 "처음부터 N개를 건너뛰고 그 다음 M개를 보여줘" 라는 방식이다.

 

➡ 만약, 사용자가 페이지 1(OFFSET 0, LIMIT 5)을 보고 있는 동안 Sam이 게시글을 삭제했다고 가정해보자.

그 상태에서 페이지 2를 OFFSET 5로 조회하면, 원래 6번째에 있어야 할 게시글(예: 고양이 사진)이 5번째로 당겨져서 OFFSET 5 기준에선 건너뛰어진다. -> 즉,OFFSET 으로 건너뛴 위치가 데이터 변경으로 밀림

 

➡  만약, 반대로 페이지 1을 본 직후 Sam이 게시글을 2개 추가했다고 가정해보자.

페이지 2는 여전히 OFFSET 5 기준으로 조회된다.

근데 새로운 게시글이 상단에 추가되었기 때문에, 우리가 이미 본 게시글 중 2개가 다시 결과에 포함돼서 중복된다.

 

커서 기반 페이징

SELECT * FROM posts WHERE created_at < '2025-04-24 16:02:00' ORDER BY created_at DESC LIMIT 5;

 

반면 커서 페이징은 "마지막으로 본 데이터 이후" 를 기준으로 다음 데이터를 조회한다. created_at이나 id와 같은 고유하면서 정렬 가능한 컬럼 값을 커서로 사용하면, 실시간 데이터 변경에도 데이터가 안정적으로 이어진다.

 

이처럼 커서 페이징은 데이터의 변동성이 높은 실시간 서비스 환경에서 특히 강력하다. 누락이나 중복 없이, 자연스럽고 연속적인 데이터 흐름을 제공할 수 있다.

 

 

2️⃣ 빅데이터 환경에서의 압도적인 성능

오프셋 페이징은 offset이 커질수록 데이터베이스의 부담도 커진다. 

왜냐하면 OFFSET은 단순히 건너뛰는 것이 아니라, 내부적으로는 모든 데이터를 스캔하고 난 뒤 해당 위치에서 잘라내는 방식이기 때문이다.

 

직접 해보는 것이 가장 와닿을 수 있는 방법이기에 MySQL 을 이용하여 직접 데이터를 넣어 테스트 해보았다.

 

  • 우선, 약 250 만개의 임시 데이터를 테이블에 삽입 하였다.

 

SELECT * FROM test_data ORDER BY created_at LIMIT 5 OFFSET 1000000;

 

100만개의 데이터를 정렬한후 5개행만 조회하였다 -> 즉 ,1000001~1000005 번째의 데이터를 created_at 기준으로 오름차순으로 정렬하여 출력

 

1s 354 ms 라는 굉장히 긴 시간이 소모되었다. 왜냐하면 100만개의 데이터를 불 필요하게 조회하여 정렬까지 수행해야하기 때문이다.

 

그럼 커서를 이용하는 쿼리문을 이용하면 어떨까?

 

우선 created_at 에 인덱스를 적용하면 성능차이를 극대화 할 수 있기 때문에 해당 쿼리를 먼저 실행시켰다.

CREATE INDEX idx_created_at ON test_data (created_at);

 

다음 200만번째 정도에 있는 created_at 값을 임시로 입력하여 해당 쿼리를 실행하였다.

SELECT * FROM test_data
WHERE created_at > '2025-04-15 00:09:19'
ORDER BY created_at
LIMIT 5;

 

 

165ms 로 위의 방법 보다 10배이상 시간을 단축 할 수있었다.

 

🟩 정리

OFFSET 페이징은 데이터의 양이 많아질수록 성능이 급격히 저하된다.
실제로 250만 개의 데이터 중 100만 번째 이후 데이터를 조회할 때, 1.3초 이상이 소요되었다.

이는 OFFSET 방식이 단순히 건너뛰는 것이 아니라, 앞의 모든 데이터를 정렬하고 나서 잘라내는 구조이기 때문이다.

 

반면, 커서 기반 페이징은 불필요한 스캔 없이 조건에 맞는 소수의 데이터만 추출하기 때문에 훨씬 빠르다.
같은 조건에서 created_at 컬럼에 인덱스를 적용하고 커서를 이용해 조회한 결과,

단 165ms 만에 데이터를 응답받을 수 있었다. 

 

실제 서비스에선 해당 시간의 차이가 체감 성능에 큰 차이를 만들 것같다.

 

 

 

✅ 커서 페이징의 단점

위에선 커서 페이징의 장점만 부각하였지만 단점 또한 당연히 존재한다.

1️⃣ 제한된 정렬 기능

커서 페이징은 정렬 기준 컬럼이 유일하고 순차적일 때 가장 잘 동작한다. 그래서 created_at, id, email 같은 필드를 커서로 자주 사용한다. 반면 first_name, last_name 같은 중복 가능성이 큰 필드를 커서로 쓰기엔 애매하다.

복합 정렬 기준(예: 이름 + 생일)을 커서로 만들 수 있지만, 이 경우 쿼리 복잡도가 증가하고 오히려 성능이 저하될 수 있다.

 

2️⃣ 구현이 간단하진 않다

커서 페이징은 오프셋보다 구현 난이도가 살짝 있다. 기본적으로 커서를 암호화하거나, 다음 페이지의 커서를 프론트에 전달하고, 다시 이를 백엔드에서 복호화해 처리해야 하기 때문이다. 또한 커서 기반 쿼리도 정렬 조건에 따라 다르게 구성해야 한다.

 

3️⃣ 무한 스크롤 중독(?) 가능성

커서 페이징은 무한 스크롤과 찰떡궁합이다. 그렇기 때문에 사용자들이 의도하지 않게 더 많은 콘텐츠를 소비하게 될 수 있다.

UX 측면에서 이게 장점일 수도 있지만, 사용자의 주의 집중력이나 시간 소비 측면에선 단점으로 볼 수도 있다.

 

 

✅ 오프셋은 안쓰나요?

이쯤 오면 오프셋은 그럼 안쓰는 건가? 의문이 들 수 있다. 

절대 그렇지 않다 . 오프셋 페이징이 유용할때도 있다.

 

  • 정적이고 실시간성이 필요 없는 데이터
  • 특정 페이지를 바로 조회하고 싶은 경우 (ex: 3페이지로 바로 이동)
  • 정렬 기준 컬럼이 고유하지 않은 경우
  • 개발 시간이 촉박하고 간단하게 구현하고 싶은 경우

 

📋 정리 

무조건 커서 페이징이 좋다는 생각보다는, 서비스의 특성과 사용자의 니즈에 따라 유연하게 선택하는 것이 중요할 거 같다.

 

👉 정적 목록 → 오프셋
👉 실시간 피드 → 커서

 

참고

https://medium.com/swlh/how-to-implement-cursor-pagination-like-a-pro-513140b65f32

 

How to Implement Cursor Pagination Like a Pro

So you’ve decided to implement cursor pagination in your website. Well, you’ve come to the right place! (If you’re not entirely convinced…

medium.com

 

  • 위의 링크는 facebook 을 개발할 때 고민했던 페이지네이션에 대한 글이다.프로젝트를 진행할 때 참고하면 많은 도움이 될 거 같다.

 

 

 

 

 

 

 

 

'Spring' 카테고리의 다른 글

[Spring] 트랜잭션 내부 호출 문제  (1) 2025.05.01
[Spring]Builder 패턴 vs 생성자, 무엇이 더 나을까?  (3) 2025.04.28
[Spring/JPQL] JPQL이 무엇인지 알고 사용하자  (1) 2025.04.18
[Spring]Converter 보다 세부적으로 타입변경하는 법  (0) 2025.04.16
[Spring]HttpMessageConverter  (0) 2025.04.16
'Spring' 카테고리의 다른 글
  • [Spring] 트랜잭션 내부 호출 문제
  • [Spring]Builder 패턴 vs 생성자, 무엇이 더 나을까?
  • [Spring/JPQL] JPQL이 무엇인지 알고 사용하자
  • [Spring]Converter 보다 세부적으로 타입변경하는 법
코딩로봇
코딩로봇
금융 IT 개발자
  • 코딩로봇
    쟈니의 일지
    코딩로봇
  • 전체
    오늘
    어제
    • 분류 전체보기 (137) N
      • JavaScript (8)
      • SQL (10)
      • 코딩테스트 (30) N
        • Java (15)
        • SQL (13) N
      • Java (10)
      • 프로젝트 (22) N
        • 트러블슈팅 (7)
        • 프로젝트 회고 (13) N
      • git,Github (2)
      • TIL (36) N
      • Spring (17) N
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    java #arraylist #list #배열
    스파르타 코딩 #부트캠프 #첫ot
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
코딩로봇
[Spring]오프셋 페이징보다 커서 페이징을 써야하는 이유
상단으로

티스토리툴바