Pagination(offset vs cursor)

503·2022년 10월 18일
2

발표스터디

목록 보기
4/4

안녕하세요, 503입니다.

이번 발표스터디의 4주차 주제로는 Pagination으로 정해보았습니다.

웹페이지를 로드할 때, 동시에 모든 데이터를 다 불러와서 처리하게 된다면 성능에 큰 문제가 생기겠죠? 따라서 페이지네이션은 거의 필연적으로 구현하게 되는 기능입니다.


페이지네이션? (Pagination, 페이징)

특정한 정렬기준 + 필요한 개수의 조건에 맞춰 데이터를 가져오는 것

보통 필요한 개수를 지정하고 상황에 맞춰 정렬기준이 조건에 추가되는데, 이러한 조건을 맞춰 데이터를 가져오는 것을 페이지네이션, 줄여서 페이징이라고 합니다.

페이지네이션을 처리하는 방법으로는 크게

  1. 오프셋 기반 페이지네이션(offset-based pagination)
  2. 커서 기반 페이지네이션(cursor-based pagination)

이 두 가지가 있습니다.


1. 오프셋 기반 페이지네이션(offset-based pagination)

DB의 limit, offset 쿼리를 사용하여 구분하여 ‘페이지’ 단위로 구분하여 요청/응답하게 구현

우리가 아는 일반적인 방법의 페이징입니다.

  • 비교적 구현이 매우 간단하고, 원하는 페이지로 쉽게 뛰어넘어볼 수 있음
  • 데이터가 몇 번째에 있는지 집중

오프셋 페이징 구현

MySQL에서는 LIMIT 쿼리를 사용하면 간단히 가능합니다.

  • n개의 데이터를 page-1만큼 skip하고 그 다음부터 n개의 데이터를 요청하는 식
SELECT * FROM post ORDER BY id DESC LIMIT 5,10; // 5개 skip후 10개 요청
SELECT * FROM post ORDER BY id DESC LIMIT 15 OFFSET 10;
SELECT * FROM post ORDER BY id DESC LIMIT 25 OFFSET 10; .....

2. 커서 기반 페이지네이션(cursor-based pagination)

클라이언트가 가져간 마지막 row의 순서상 다음 row들을 n개 요청/응답하게 구현

  • 주로 무한 스크롤이나 더보기를 구현할 때 사용(SNS)
  • 어떤 데이터의 다음에 있다는데에 집중

커서 페이징 구현

  • cursor가 가리키는 것 다음부터 n개의 데이터를 요청하는 식

아래와 같은 데이터가 있고 생성일자와 수정일자로 정렬한다고 가정해봅시다.

id생성일자수정일자
123117
124218
114 ✅419
115420

생성일자와 수정일자는 이해하기 쉽게 간략히 표현했습니다.
이때, id가 114의 다음 페이지의 데이터를 얻기 위한 쿼리는 하기와 같습니다.

SELECT * FROM post
WHERE (생성일자 > 4) or (생성일자 = 4 and 수정일자 > 19 )
ORDER BY 생성일자 ASC, 수정일자 ASC LIMIT 4;

하지만 이렇게 OR절로 구현하는 경우 2가지 문제가 발생합니다.

  1. 대부분의 RDBMS는 WHERE에 OR-clause 를 사용하면 인덱싱을 제대로 못 태움.
    => OR 연산자에 관계된 모든 컬럼이 복합키로 설정되어 있지 않다면 인덱스를 사용하지 않고 전체조회(풀 테이블 스캔)을 함 (후자가 더 빨라서..)
  2. 클라이언트가 ORDER BY에 걸려있는 모든 필드를 알아야하고, 매 페이지 요청시마다 이 값들을 전부 보내야 함.

⇒ 그러므로 WHERE절에 걸리는 조건들을 이용해서 고유한 값인 cursor(커서)를 만듭니다.


커스텀 cursor 생성하기

각 생성/수정일자를 최대 4자라고 가정하면 이 두가지를 합쳐서 “00040022” 와 같은 식으로 가공하여 cursor(커서) 값으로 사용합니다.

id생성일자수정일자cursor
12311700010017
12421800020018
11441900040019 ✅
11542000040020

아까 위의 쿼리를 cursor를 사용하도록 수정하면 하기와 같습니다.

SELECT *, CONCAT(LPAD(POW(생성일자,4,'0'), LPAD(POW(수정일자,4,'0')) AS `cursor`
FROM post
WHERE cursor > "00040019"
ORDER BY cursor ASC
LIMIT 4;
  • CONCAT : MySQL 내장함수, 문자열 합침 (숫자인 경우 문자열 자동변환)
  • LPAD : MySQL 내장함수, 문자열 / 숫자를 지정된 길이의 문자열로 채움 (왼쪽)

숫자값인 데이터를 LPAD를 이용해 6자 길이의 고정문자열로 만들고 이를 CONCAT으로 붙여 커스텀 cursor를 생성합니다.


👿 오프셋 페이징의 문제

오프셋 페이징은 비교적 구현하기에 간단하지만, 문제점이 크게 2가지가 있는데요.
예시를 들며 설명해보겠습니다.

1-1. CREATE 시 데이터 중복 출력의 문제

유저1이 1페이지에 접속해서 5개의 데이터(가~마)를 확인했습니다.
이후, 유저2가 새 데이터 를 새롭게 등록했습니다.

❓ 이 때, 유저1이 2페이지에 접속하면 어떻게 될까요?

⇒ 1페이지에서 본 데이터 라,마중복해서 보게됩니다.


다른 예시를 들어보겠습니다.

웹사이트에 많은 사람들이 동시에 접속해서 많은 글을 작성한다고 가정해봅시다! 초당 10개의 새로운 데이터가 들어오고, 한 페이지 당 10개의 데이터를 보여줍니다.

❓ 이 때, 1페이지의 10개의 데이터를 본 사람이 1초 뒤 2페이지를 눌렀을 때 어떻게 될까요?

=> 1페이지에서 봤던 10개의 데이터를 중복해서 그대로 보게 됩니다.


즉, 각각의 페이지를 요청하는 사이에 데이터의 변화가 있는 경우 중복 데이터가 노출되는 문제가 발생하게 되는 것입니다.


1-2. DELETE 시 데이터 누락의 문제

유저가 1페이지에 접속해서 5개의 데이터(가~마)를 확인합니다.
이후 관리자가 가,나의 데이터를 삭제했습니다.

이 때, 사용자가 2페이지를 클릭하면 볼 수 있는 데이터는 무엇일까요?

⇒ 사용자는 재접속하지 않는 이상 a,b를 볼 수 없게됩니다. (데이터 누락)

offset 페이징은 데이터가 수정되는 것을 신경쓰지 않으며, 단순히 쿼리 결과문의 다음 5개 데이터를 받기 때문에 이련 현상이 발생합니다.


2. row 개수에 따른 성능 문제

데이터는 정렬 기준(ORDER BY)에 따라서 row의 순서가 바뀌게 되는데, DB는 모든 경우에 따른 rownum을 가지고 있지 않기 때문에, 해당 row가 몇번째 순서를 갖는지 알지 못합니다.

그렇기 때문에 SQL 내장 OFFSET 절의 내부 동작방식을 본다면

요청한 데이터를 바로 조회❌
=> 이전의 데이터를 모두 조회 후 그 ResultSet에서 offset을 조건으로 잘라내는 방식

입니다. 이는 row의 수가 많아지고 offset 값이 올라갈수록 쿼리의 퍼포먼스가 떨어지는 문제가 발생합니다.

offset vs cursor 페이징의 시간 복잡성 비교

offset vs cursor 페이징의 시간 복잡성을 비교해보면 아래와 같습니다.

  • 오프셋 페이징 : O(N), O(offset+limit)로 offset이 커질수록 시간증가해 UX 감소
  • 커서 페이징 : O(1), O(limit) 로 항상 일정

그렇다고 무조건 오프셋 페이징을 사용하면 안되는 것일까요? ❌


😦 커서 페이징의 단점

커서 기반 페이징이 효율적이며, 가능한 사용되는 것이 좋지만 단점이 존재하지 않는 것은 아닙니다.

1. 제한된 정렬 기능

커서 페이징을 사용하려면 특징 지점으로 커서를 지정할 수 있어야하고, 때문에 필요한 요구사항으로 아래와 같은 조건이 충족되어야합니다.

❗ 정렬할 컬럼에 중복된 값이 존재하면 안되고, 순차적이어야 한다.

이러한 요구사항 때문에 대부분의 커서 페이징은 timestamp컬럼을 기준으로 합니다. 작은 단위의 timestamp는 순차적이고 고유하기 때문입니다.

고유하지 않은 컬럼으로 정렬한다면 오프셋 페이징보다 더 느려질 수 있다.

2. 까다로운 구현

오프셋 페이징의 구현이 무척 쉽기 때문에 그에 비해 커서 페이징이 더 까다롭게 느껴질 수 있습니다.
따라서 시간이 부족하고, 실시간 데이터가 아니라면 굳이 커서로 할 필요는 없습니다.




결론

offset 기반 페이징 사용해도 되는 경우

  • 애초에 row 수가 적어서 퍼포먼스 걱정이 필요 없는 경우
  • 데이터의 변화(생성, 삭제)가 거의 없어서 중복 데이터가 노출될 일이 없는 경우
  • 일반 유저를 위한 리스트가 아니어서 중복데이터가 노출되어도 문제가 없는 경우(관리자 페이지 등)
  • 검색엔진이 인덱싱할 이유, 유저가 마지막 페이지 갈 이유, 오래된 데이터의 링크가 공유될 이유도 없는 경우

⇒ 그게 아니라면? cursor 기반을 사용하는 것이 좋다




🔗 참고

profile
얼레벌레 개발자로 살아가기. 개발하면서 만났던 이슈를 기록합니다.

0개의 댓글