Description
부제 : 페이스북이 커서 기반 페이지네이션을 선택한 이유
원문 <Is offset pagination dead? Why cursor pagination is taking over>
페이스북 개발자 페이지는 이렇게 말합니다.
"커서 기반 페이지네이션은 가장 효율적인 페이징 기법이고 가능한 커서 기반으로 구현해야 한다."
그렇다면 페이지네이션이란 무엇일까요?
페이지네이션은 데이터 집합에서 데이터를 분리하는 과정입니다. 책의 페이지들과 같죠.
웹사이트에서 페이지네이션을 구현하는 두 가지 주된 방법이 있습니다. 그렇다면 어떤 방법이 더 좋은 걸까요? 두 가지 방법을 살펴봅시다.
오프셋 기반 페이지네이션
이 방법은 수십 년 동안 효율적으로 사용되었습니다. 다음과 같이 OFFSET
값을 가지는 SQL 쿼리를 활용합니다.
SELECT * FROM table
ORDER BY timestamp
OFFSET 10
LIMIT 5
명시적으로 페이지 수를 보여주어 사용자가 선택할 수 있는 프론트엔드와 함께 쌍을 이루는 경우가 많습니다. 마치 책의 페이지를 넘기는 것과 비슷하죠.
커서 기반 페이지네이션
이 방법은 다음과 같이 (보통 타임스탬프를 비교하는) <
, >
와 같은 비교 연산자를 포함하는 SQL 쿼리를 활용합니다.
SELECT * FROM table
WHERE cursor > timestamp
ORDER BY timestamp
LIMIT 5
여러분이 많은 실시간 데이터를 다루는 웹사이트(페이스북, 슬랙, 트위터 등과 같은)를 보고 있다면 커서 기반 페이지네이션이 동작하는 것을 볼 수 있습니다.
커서 기반 페이지네이션은 프론트엔드에 무한 스크롤 데이터를 제공하기 위해 선호되는 방법입니다. 무한 스크롤은 여러분이 페이스북 피드를 내리다가 바닥에 다다랐을 때, 더 많은 피드들이 어떻게 더 나타났는지 생각해보시면 됩니다.
그런데 왜 이렇게 많은 기업들이 오프셋 기반에서 커서 기반으로 전환한 것일까요?
커서 기반 페이지네이션의 장점
1. 실시간 데이터의 제공이 뛰어나다
아마도 커서 기반 페이지네이션의 가장 큰 장점은 효율적으로 실시간 데이터를 다룰 수 있다는 것입니다. 왜냐하면 커서들은 데이터를 정적으로 유지할 필요가 없기 때문이죠. 즉, 아이템들이나 행들이 추가되고 삭제된다 해도 각 새로운 페이지들이 여전히 올바르게 로드된다는 겁니다.
지난 10년간 real-time 어플리케이션의 해일 속에서, 커서 기반 페이지네이션 방법은 큰 탄력을 얻게 되었습니다.
기존 오프셋 기반 페이지네이션을 사용했을 때 만약 데이터가 고정된 상태가 아니라면 두 가지 상황이 발생할 수 있습니다.
- 데이터가 스킵됨
- 동일한 데이터가 2번 반환됨
이 두 가지 상황에 대해서 좀 더 살펴보겠습니다.
2. 스킵되는 데이터가 없다
데이터가 스킵되는 실시간 예제를 살펴봅시다. 여러분이 페이스북 뉴스피드에 처음 도달했을 때 여러분은 친구들이 작성한 최근 5개의 게시물을 바로 볼 수 있습니다.
- 왕좌의 게임 밈
- 샘이 술에 취해 올린 창피한 사진들...
- 정치적 주장
... 등등. 다음 페이지의 데이터를 로드할 때 다음과 같은 새로운 게시물이 로드됩니다.
- 카렌 이모의 고양이 사진
- 친구 올리비아의 호주 여행 추억팔이 사진들
- 오프라의 인상적인 인용구들
여러분은 위의 두 페이지를 기대했겠지만 여러분이 페이지 1을 보고 있는 동안 샘이 잠과 술에서 깨어 어젯밤에 올린 자신의 사진을 지우기로 했다고 가정해봅시다.
여러분이 더 많은 게시물을 로드하기 위해 스크롤 했을 때, 갑자기 여러분이 보는 페이지 2는 이와 같을 것입니다.
잠시만! 고양이 사진들은 어디 간걸까요?
불행하게도 오프셋 기반 페이지네이션은 데이터가 수정된 것에 대해 신경쓰지 않습니다. 단지 다음 5개의 게시물에 대해 데이터베이스에 쿼리를 날릴 뿐이죠. 이 5개의 게시물은 여전히 5의 오프셋에 위치할 것입니다. 데이터베이스는 매번 다음과 같이 쿼리될 겁니다.
SELECT * FROM table ORDER BY timestamp OFFSET 0 LIMIT 5
SELECT * FROM table ORDER BY timestamp OFFSET 5 LIMIT 5
SELECT * FROM table ORDER BY timestamp OFFSET 10 LIMIT 5
SELECT * FROM table ORDER BY timestamp OFFSET 15 LIMIT 5
...
각 라인은 페이지 1, 페이지 2, 페이지 3 등을 의미합니다.
이는 여러분이 귀여운 고양이 사진들을 볼 수 없다는 것입니다! 그리고 더욱 안타깝게도 여러분은 고양이 사진을 잃어버렸다는 것을 알 수도 없다는 거죠. 이게 다 샘 때문이예요!
커서 기반 페이지네이션은 인덱스로 사용되는 오프셋 파라미터를 사용하는 대신 데이터셋의 특정 레코드의 포인터 역할을 하는 커서 파라미터를 사용하여 마지막 페이지가 중단된 위치를 나타냅니다. 예를 들어 이 커서는 September 12, 4:02 PM 타임스탬프를 가지는 데이터를 마지막으로 받았다고 데이터베이스에게 알려주어 해당 타임스탬프 이전의 5개의 게시물을 반환받습니다.
SELECT * FROM table WHERE cursor > timestamp ORDER BY timestamp LIMIT 5
이 방법은 샘이 두 장의 사진을 지운 것과 상관없이 고양이 사진이 포함된 이전 데이터들을 가져올 수 있는 겁니다. 정말 다행이죠.
3. 중복 데이터가 없다
두 번째 시나리오를 생각해봅시다. 여러분이 페이지 1을 보고 있고 샘은 여전히 술에 취해 사진을 더 올린다고 가정하겠습니다. 여러분이 페이지 2를 보려고 할 때 오프셋이 중복되는 문제가 발생합니다.
저도 취한 걸까요? 아니면 이모가 취한 걸까요?
아니요. 오프셋이 5가 아닌 6에서 시작되어야 하는데 여러분은 게시물 1개가 추가됨에 따라 보여지는 데이터가 중복되버린 것입니다.
이 상황은 사용자에게 문제가 있다는 것을 알려주고 실제로는 잘 작동하고 있지만 불안정하게 보이기 때문에 아주 최악의 시나리오입니다.
4. 효율적으로 대량의 데이터를 다룬다
여러분은 SQL의 OFFSET
절이 어떻게 작동하는지 궁금하신 적이 있나요? 저 역시 그렇지 않았습니다. 커서 기반이 가장 효율적인 페이지네이션이라고 페이스북에서 주장하기 전까지는 말이죠.
제가 찾아본 것은 저를 놀라게 했습니다.
오프셋은 단순히 레코드를 선택하기 전에 데이터베이스가 건너뛸 레코드 수입니다.
즉, 데이터베이스에 요청한 다음 데이터셋을 반환해주는 것이 아니라 이전에 제공했던 데이터들을 모두 스캔한다는 것인데요. 오프셋 수가 증가할 수록 이는 큰 문제거리가 됩니다.
약 730만 개의 레코드로 구성된 데이터셋에서 커서 기반 쿼리와 오프셋 기반 쿼리를 모두 테스트하기 전까지는 문제가 완전히 계산되지 않았습니다.
여기서 실제 사용자 경험으로 확대해 보겠습니다. 사용자가 700만 번째 행을 표시하는 페이지로 이동해본다고 가정하죠.
커서 기반 페이지네이션은 사용자가 다음 또는 이전 버튼을 눌렀을 때 새로운 페이지가 로드되기까지 약 0.18초가 소요됩니다.
오프셋 기반 페이지네이션은 동일한 동작임에도 13초가 걸리는 겁니다! 이는 각 페이지마다 데이터를 로드하는 데 13초가 걸린다는 거고 컴퓨팅 세계에서는 영원히 로드한다는 것과 같죠!
실제로는 여러분들의 사용자가 13초를 기다리기보다는 사이트에 문제가 생겼다고 생각하고 다른 곳으로 이동할 가능성이 더 높습니다.
사용자가 실제로 13초를 기다려서 해당 페이지로 이동할 확률은 얼마나 될까요? 가능은 하겠지만, 아마 거의 그럴 확률은 희박하다고 말씀드릴 수 있습니다.
그러나 700만 번째 레코드 이전에도 시간복잡도가 linear한 O(N), 더 구체적으로는 O(offset+limit)을 가지기 때문에 사용자의 UX는 감소하기 시작합니다. 반면에 커서 기반 페이지네이션은 O(1), 구체적으로는 O(limit)의 시간 복잡도를 가집니다.
위 그래프에서도 확인할 수 있습니다.
여러분이 피드를 계속 거슬러 올라갈수록 페이지 로딩이 느려지는 이유에 대해 궁금한 적이 있었다면, 오프셋 기반 페이지네이션의 비효율성을 경험해봤기 때문일 것입니다.
커서 기반 페이지네이션의 단점
그렇다면 정말 오프셋 기반 페이지네이션은 사장된 것일까요?
음, 꼭 그렇지는 않습니다. 여러분이 친구들을 철야에 초대하기 전에 오프셋 기반이 더 적합한 경우를 몇 가지 살펴보겠습니다.
페이스북은 커서 기반 페이지네이션을 가능한 사용되어야 한다고 했습니다. 그러나 어떤 상황에서는 불가능하거나, 커서 기반이 현실적이지 않을 때도 분명히 있습니다.
1. 정렬 기능이 제한된다
여러분이 성과 이름으로 정렬할 수 있는 유저 테이블을 만든다고 가정해봅시다. 구현하는 데 필요한 요구사항이 정렬 기준으로 사용할 고유한 순차 열이기 때문에 커서 기반을 사용하는 데 문제가 발생할 수 있습니다. 커서를 사용하면 데이터 집합의 특정 위치를 가리키고, 해당 레코드 다음에 레코드를 생성할 수 있다는 것을 기억하세요.
즉 대부분의 커서 구현은 타임스탬프 열을 기반으로 합니다. 확실하게 순차적이고 시간을 세분화된 수준으로 추적하면 해당 열이 고유하다고 합리적으로 가정할 수 있습니다.
이름
은 순차적일 수 있지만, 고유하지는 않습니다. 우리는 주위에 동명이인들을 많이 봤으니까요. 그래서 커서 기반으로 구형하게 되면 데이터의 스킵이나 중복된 결과를 받는 문제에 봉착하게 될 것입니다.
커서 기반 페이지네이션과 함께 사용자 정렬을 구현하려면 사용자 아이디나 이메일과 같이 순차적이며 고유한 열을 사용하는 것이 더 좋습니다.
그러나 이름이나 성으로 정렬해야 할 때는 커서 기반 페이지네이션이 최선의 방법이 아닐 수 있습니다. 이름과 성을 이어서 고유한 컬럼으로 생성할 수도 있고, 여러 튜플을 사용하여 고유한 컬럼을 사용할 수도 있지만 이렇게 하면 오프셋을 사용하는 것보다 느려질 정도로 속도가 감소합니다. SQL 문의 연결이나 튜플을 비교하는 것이 O(N), 구체적으로는 O(전체 데이터셋)의 시간복잡도를 가지기 때문입니다.
그리고 사실 첫 번째 페이지가 가장 느리게 로드될 겁니다. 이상하고 직관적이지는 않지만 SELECT 문은 나아갈수록 더 많은 레코드를 제외하게 됩니다.
여기에서 이 이슈를 해결할 몇 가지 트릭이 있습니다. 참고 : 이 현상에 대해서
2. 커서는 구현하기 더 까다로울 수 있다
커서가 반드시 구현하기 어려운 것은 아니지만, 오프셋은 구현하는 것이 매우 쉽습니다. 페이지네이션을 구현하는 빠른 방법을 찾고 있다면 오프셋을 사용하는 것이 좋습니다. 특히 실시간 데이터를 다루는 것이 아니라면요. 필요하지 않은데 일을 복잡하게 만들 필요는 없습니다.
직접 커서를 구현해본 결과, 90% 사례에서 커서를 구현하는 것이 그리 어렵지는 않았습니다. 그러나 오프셋보다는 고려해야 할 엣지 케이스가 훨씬 더 많았습니다. 다시 말하자면, 수정하는 것이 어렵지는 않지만 일반적으로 오프셋보다는 어렵습니다.
따라서 커서를 구현하는 데 시간이 더 걸릴 수 있습니다. 여러분, 여러분의 회사 또는 PM들이 해야 하는 trade-off일 뿐입니다.
3. 무한 스크롤은 중독될 수 있다
이것은 정확히 말하자면 오프셋 기반에서도 무한 스크롤이 구현은 가능하기 때문에 커서 기반 페이지네이션의 이슈는 아닙니다. 그러나 커서의 사용이 증가하면서 앞서 언급한 데이터 중복 문제 없이 효율적으로 무한 스크롤의 구현이 가능했기 때문에 실시간 어플리케이션에서 무한 스크롤의 빈도가 증가했습니다.
무한 스크롤은 사용자가 많은 데이터를 한번에 볼 수 있는 편리한 UI이기는 하지만, 사용자가 다음 페이지를 보기 위해 버튼을 누르는 의식적인 결정 과정이 사라지게 합니다. 이는 사용자가 더 많은 게시물을 로드하고 있음을 알아채지 못할 수 있어 더욱 중독성이 높은 경험을 하게 됩니다.
커서의 구현
모든 열을 기준으로 정렬할 때 약간의 문제가 발생하더라도 실기간 데이터를 다룰 때는 커서 기반 페이지네이션만큼 좋은 것이 없습니다.
왜 페이스북 같은 기업들이 커서 기반 페이지네이션에 대해 과감한 발언을 하는 지 이해가 되기 시작했습니다. 그리고 여러분의 웹사이트가 커서 기반 페이지네이션을 사용했을 때 얻을 수 있는 효과들이 있는지 자문해볼 때 입니다.
커서를 사용하는 것이 더 낫다는 확신이 든다면, 웹사이트에서 커서로 구현하는 방법에 대한 스타트업 기사를 읽어보세요!
축하합니다!! 😎 드디어 끝까지 다 읽으셨어요. 이 아티클이 마음에 드셨다면 👌, 아래 clap 버튼을 눌러주세요 👏. 저에게 큰 힘이 될 겁니다. 또 다른 사람들이 이 아티클을 볼 수 있게 한답니다.