![[실전 DB 10] 키셋 페이지네이션, 건너뛰기 없이 이어가기](https://img.thenullpage.com/posts/5702/5702_1_bc67a5.webp)
앞 편에서 페이지네이션의 근본 해법으로 커서 방식을 언급했다. 흔히 키셋 페이지네이션이라고도 부른다. 마지막으로 본 지점을 기준으로 그다음을 가져오는 방식이라, 개수 세기도 건너뛰기도 없다. 이번 편은 이 키셋 페이지네이션이 어떻게 동작하고 왜 효율적인지, 그리고 적용할 때 무엇을 조심해야 하는지를 다룬다.
이건 업계에서 대규모 목록을 다루는 정석으로 자리 잡은 방식이다. 처음엔 낯설어도 원리를 이해하면 왜 정석인지 자연스럽게 납득된다.
1. 건너뛰기 방식의 한계 복습
먼저 앞 편의 건너뛰기 방식을 복습하자. 몇 번째부터 몇 개를 가져오는 이 방식은 두 가지 비용이 있었다. 하나는 전체 개수를 세는 것, 다른 하나는 뒤쪽 페이지에서 앞의 것들을 건너뛰며 훑는 것이다. 둘 다 전체나 그 앞을 훑는 방식이라 근본적으로 무겁다.
특히 건너뛰기가 문제다. 백 번째 페이지를 가져오려면 데이터베이스는 앞의 아흔아홉 페이지 분량을 하나씩 세며 지나가야 한다. 원하는 건 백 번째 페이지의 몇 개뿐인데, 거기 도달하려고 방대한 앞부분을 훑는 것이다. 뒤로 갈수록 이 비용이 커진다. 봇이 깊은 페이지를 긁으면 이 비용이 폭증한다.
이 한계는 건너뛰기라는 방식 자체에서 온다. 몇 번째라는 위치로 접근하니, 그 위치에 가려면 앞을 다 지나야 한다. 위치는 상대적인 값이라, 앞이 얼마나 있는지 세지 않고는 도달할 수 없다. 이걸 근본적으로 없애려면 위치가 아니라 값으로 접근해야 한다. 그게 키셋 방식이다.
여기서 하나 구분할 게 있다. 건너뛰기 방식이 무조건 나쁜 건 아니다. 데이터가 적고 앞쪽 몇 페이지만 보는 화면이라면 충분히 쓸 만하다. 구현이 단순하고 페이지 번호도 자연스럽게 나온다. 문제는 데이터가 커지고 뒤쪽까지 접근이 일어날 때다. 그 조건에서만 키셋이 필요해지는 것이라, 상황을 보고 고른다.
2. 키셋 방식의 원리
키셋 방식은 몇 번째가 아니라 이 값 이후를 가져온다. 예를 들어 목록을 시각순으로 보여준다면, 마지막으로 본 글의 시각을 기억해뒀다가, 다음엔 그 시각보다 이전 글들을 가져온다. 위치가 아니라 마지막 값을 기준점으로 삼는 것이다. 이 기준점을 커서라고 부른다.
이게 왜 빠르냐면, 그 기준점을 인덱스로 바로 찾을 수 있기 때문이다. 시각 인덱스가 있으면 특정 시각의 위치를 바로 찾아, 그다음부터 필요한 개수만 읽는다. 앞을 건너뛸 필요가 전혀 없다. 인덱스로 시작점을 찍고 거기서부터 딱 필요한 만큼만 읽으니, 몇 번째 페이지든 읽는 양이 일정하다. 뒤쪽 페이지도 앞쪽만큼 빠르다.
가상의 예로, 시각순 목록의 다음 페이지를 가져올 때 WHERE created < 마지막본시각 ORDER BY created DESC LIMIT 개수 같은 쿼리가 된다. 마지막 본 시각 이전 것들을 인덱스로 찾아 필요한 개수만 읽는다. 전체를 세지도, 앞을 건너뛰지도 않는다. 첫 페이지든 천 번째 페이지든 하는 일이 똑같다. 이게 키셋의 핵심이다.
내가 이 방식으로 바꾸고 나서 측정해 보니, 깊은 페이지의 읽기량이 첫 페이지와 사실상 같아졌다. 예전엔 뒤로 갈수록 치솟던 그래프가 평평해진 것이다. 봇이 아무리 깊이 파고들어도 각 요청의 비용이 일정하니, 봇 트래픽이 데이터베이스에 주는 부담의 성격 자체가 달라졌다.
이 일정함이 주는 안정감이 크다. 건너뛰기 방식에서는 어떤 요청이 얼마나 무거울지 예측하기 어려웠다. 누가 몇 번째 페이지를 요청하느냐에 따라 비용이 널뛰었기 때문이다. 키셋으로 바꾸니 모든 요청의 비용이 비슷해져, 부하를 예측하고 대비하기가 쉬워졌다. 최적화는 평균만 낮추는 게 아니라 이렇게 들쭉날쭉함도 줄인다.
키셋이 인덱스와 얼마나 잘 맞는지도 짚어둘 만하다. 정렬 기준으로 쓰는 값에 인덱스가 걸려 있으면, 그 인덱스는 이미 그 값 순서로 정렬돼 있다. 커서가 가리키는 지점을 인덱스에서 바로 찾아, 정렬된 순서 그대로 필요한 만큼만 읽는다. 앞서 다룬 인덱스가 여기서 페이지네이션과 맞물려 힘을 발휘하는 것이다.
3. 타이브레이커의 필요
키셋 방식에서 주의할 게 있다. 기준 값이 겹칠 때다. 같은 시각에 여러 글이 있으면, 시각만으로는 어디까지 봤는지 정확히 구분이 안 된다. 그 시각의 글 중 어디까지 봤는지 애매해진다. 그러면 경계에서 일부가 빠지거나 중복으로 나올 수 있다.
그래서 겹치지 않는 보조 기준을 함께 쓴다. 시각이 같으면 각 글의 고유한 식별자로 순서를 정하는 것이다. 시각과 식별자를 함께 기준점으로 삼으면, 시각이 겹쳐도 식별자로 정확히 구분된다. 이 보조 기준을 타이브레이커라고 한다. 키셋 방식에는 이 타이브레이커가 사실상 필수다.
내가 키셋을 처음 적용할 때 이 타이브레이커를 빠뜨려서, 같은 시각의 글이 목록 경계에서 사라지는 문제를 겪었다. 대량으로 올라온 글들의 시각이 초 단위로 겹치면서, 페이지 경계에 걸린 글이 다음 페이지에서 누락된 것이다. 시각과 고유 식별자를 함께 기준으로 쓰니 바로 해결됐다.
구현할 때는 시각으로 크게 정렬하고 같은 시각은 식별자로 정렬한다. 그리고 커서도 시각과 식별자를 함께 넘긴다. 다음 페이지 조건은 그 시각보다 이전이거나, 시각이 같다면 그 식별자보다 앞인 것이 된다. 이 조합이 키셋을 빈틈없이 정확하게 만든다. 정렬 기준과 커서 기준이 똑같이 맞아야 한다는 게 요령이다.
이 경험으로 배운 건, 페이지네이션 같은 기본 기능도 경계에서 미묘하게 틀어질 수 있다는 것이다. 대부분의 경우엔 멀쩡히 도니 문제를 눈치채기 어렵고, 같은 시각의 글이 하필 페이지 경계에 걸린 드문 순간에만 글이 새어 나간다. 그래서 이런 건 테스트할 때 경계를 일부러 만들어 확인해야 한다. 정상 경우만 보면 못 잡는다.
4. 화면 방식의 변화
키셋 방식의 현실적인 걸림돌은 화면이다. 몇 번째라는 개념이 없으니, 몇 페이지 몇 페이지 하는 번호를 매기기 어렵다. 대신 더보기 버튼이나 무한 스크롤 방식이 어울린다. 마지막 것 이후를 계속 가져오는 방식이니, 스크롤하며 계속 이어 보는 화면과 잘 맞는다.
그래서 건너뛰기 방식에서 키셋으로 바꾸면 화면도 함께 바꿔야 하는 경우가 많다. 페이지 번호 화면을 더보기나 무한 스크롤로 바꾸는 것이다. 이 화면 변경이 부담이라, 앞 편에서 말했듯 개수 캐시 같은 저위험 해법을 먼저 하고 키셋은 여유가 있을 때 한다. 뒤와 앞을 한꺼번에 다 바꾸려 들면 사고가 나기 쉽다.
절충안도 있다. 화면은 페이지 번호를 그대로 유지하면서, 뒤에서는 현재 보는 지점을 중심으로 일정 범위만 가져오는 방식이다. 전체를 세지 않고 현재 지점 주변만 다루는 것이다. 완전한 키셋은 아니지만 건너뛰기의 비용을 크게 줄인다. 화면을 안 바꾸면서 효율을 얻는 중간 지점이라, 나도 일부 목록엔 이 절충을 썼다.
어떤 화면을 고를지는 사용자가 목록을 어떻게 쓰는지에 달렸다. 최신 글을 쭉 훑어보는 화면이면 무한 스크롤이 자연스럽고, 특정 지점으로 바로 뛰고 싶은 화면이면 페이지 번호가 편하다. 그래서 뒤의 효율만 보고 화면을 정하면 사용자가 불편해질 수 있다. 뒤의 방식과 앞의 경험을 함께 저울질하는 것이다.
5. 대규모에서 빛나는 방식
키셋 방식은 데이터가 많을수록 빛난다. 데이터가 적을 땐 건너뛰기도 견딜 만하지만, 데이터가 수십만 수백만이 되면 뒤쪽 페이지의 건너뛰기 비용을 감당할 수 없다. 키셋은 데이터가 아무리 많아도 읽는 양이 일정하니, 대규모에서 그 강점이 극명하게 드러난다.
그래서 처음부터 대규모를 예상한다면 키셋으로 설계하는 게 낫다. 나중에 데이터가 커진 뒤 건너뛰기의 한계에 부딪혀 재설계하는 것보다, 처음부터 키셋으로 가는 것이다. 다만 앞서 말했듯 화면 방식과 얽혀 있으니, 화면 설계 단계에서 이 방식을 미리 염두에 두는 게 좋다. 뒤와 앞을 함께 설계하면 나중의 큰 수술을 던다.
정작 중요한 건 데이터가 커지기 전에 이 방식을 알아두는 것이다. 막상 뒤쪽 페이지가 느려져 문제가 터진 뒤에 바꾸려면, 화면까지 얽혀 큰 수술이 된다. 미리 알고 있으면 설계 단계에서 대비하거나, 적어도 문제가 왜 생기는지 바로 진단할 수 있다. 아는 것과 모르는 것의 차이가 이런 순간에 크게 벌어진다.
정리하면 키셋 페이지네이션은 위치가 아니라 마지막 값을 기준으로 그다음을 인덱스로 찾아 읽어, 개수 세기와 건너뛰기를 없애고 뒤쪽 페이지도 빠르게 만들되, 타이브레이커가 필요하고 화면 방식과 얽힌다. 다음 편에서는 목록의 또 다른 숨은 비용, 각 행마다 딸린 정보를 따로 조회하는 반복 조회 문제를 다룬다.