※ 이 글은 위 글을 의역한 글입니다.
※ 제가 이해한 것을 토대로 약간 수정했습니다.
커서 기반 페이징이 가장 효율적인 방법이며, 가능한 항상 사용되어야 한다
-페이스북 개발자
그렇다면 페이징은 무엇일까?
페이징은 책 페이지처럼 데이터를 묶음으로 분리하는 과정이다.
페이징은 크게 2개로 나뉘는데, 둘 중 뭐가 좋을까? 한 번 알아보자.
오프셋 페이징
이 방법은 수십년동안 효과적으로 사용된 방법이다. OFFSET 값을 포함한 SQL 쿼리문을 동반한다.
SELECT * FROM table
ORDER BY timestamp
OFFSET 10
LIMIT 5
프론트엔드에서 명시적인 페이지 버튼과 함께 쓰이며, 버튼을 클릭함으로써 페이지를 휙휙 넘길 수 있다.
커서 페이징
이 방법은 실시간 데이터와 대량의 데이터(페이스북, 슬랙 , 트위터 등)을 다루는 웹사이트에서 쓰이는 페이징 방법이다.
프론트에서 무한 스크롤(인스타 그램, 페이스북처럼 하단으로 계속 스크롤 되는 페이징 방식)을 지원할 때 쓰인다.
커서 페이징이 뭐가 좋은걸까?
1. 우수한 실시간 데이터 처리능력
사실 커서 페이징의 가장 큰 장점은 실시간 데이터를 효율적으로 다룰수 있다는 점이다. 왜냐하면 커서는 데이터를 정적으로 유지할 것을 필요로 하지 않기 때문이다. 즉, 새로운 데이터가 추가되거나 제거될 수 있고, 그 데이터들은 정상적으로 조회될 것이다.
지난 수년간 실시간 어플리케이션의 큰 변화 덕분에, 커서페이징은 많은 이점을 얻었다.
만약 데이터가 정적이지 않다면, 기존의 오프셋 페이징으로는 둘 중 하나의 문제점이 발생한다.
- 누락된 데이터
- 중복된 데이터
이 문제점들에 대해서는 차근차근 자세히 살펴보자.
2. 누락되지 않는 데이터
페이스북이 스크롤페이징을 커서페이징이 아닌, 오프셋페이징으로 구현했을 경우 데이터가 누락될 수 있다.
실생활에 예를 들어서 살펴보자. 페이스북에서 친구들이 가장 최근에 올린 5개의 피드를 바로 받았다.
- Game of Thrones meme(2)
- Sam Drunk Photo(2)
- Political Rant
그리고 다음 페이지로 스크롤하면 아래의 데이터를 볼 수 있다.
- Cat Photo(2)
- Olivia's Ausralia Trip(2)
- Oprah Wisdom
사진의 2페이지를 볼 수 있을 것이라고 예상할 수 있다. 그런데 1페이지를 보고있는 동안 숙취를 앓고 있는 친구 Sam이 일어나서 술에 취한 부끄러운 사진을 삭제하는 경우를 생각해보자.
2페이지로 스크롤하면 아래 그림과 같은 피드를 받아진다.
우리는 고양이 사진을 볼 수 없다. 이전의 게임 정보, Sam의 사진, 정치 의견과 호주 여행 사진 2개와 오프라 인용문 3개의 피드만 볼 수 있을 뿐이다. 왜냐하면 오프셋 페이징은 데이터가 수정되는 것을 신경쓰지 않는다. 그저 단순히 쿼리 결과문의 다음 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, 4, ... 를 조회할 때 쓰인다.
즉 아마 페이스북을 재접속하지 않는 이상, 고양이 사진을 절대 볼 수 없을 것이다. 게다가 누락된 피드가 있다는 것을 알 방법이 전혀 없다.
그럼 커서페이징은 어떻게 수행될까? OFFSET 파라미터를 인덱스로 전달하는 오프셋 페이징과 달리 커서 페이징은 조회가 중단된 마지막 페이지를 담은 커서라는 포인터(DataSet의 특정 레코드, 로우)를 파라미터로 전달한다. timestamp 열을 기준으로 하는 커서가 있다고 가정했을 때, 커서는 마지막으로 조회된 피드의 타임스탬프가 9월 12일 16시 2분임을 알려주고 이 시간 전의 5개의 피드를 불러온다.
SELECT * FROM table WHERE cursor > timestamp ORDER BY timestamp LIMIT 5
이렇게하면 Sam이 사진을 삭제한 것은 중요하지 않다. timestamp를 기준으로 하기 때문에 고양이 사진을 포함해 정상적으로 데이터를 얻을 수 있다.
3. 중복되지 않는 데이터
위의 사진 1페이지를 보는 동안 Sam이 어젯밤에 찍은 사진 2개를 추가했다고 가정해보자. 그럼 2페이지로 스크롤하면 오프셋 페이징은 고양이 사진 중복으로 보여줄 것이다.
왜냐하면 새로운 피드가 2개 추가되어 2페이지는 OFFSET 7번부터 5개를 보여줘야하지만 여전히 5번부터 5개를 보여주기 때문이다.
이건 최악의 상황이다. 사용자는 뭔가 잘못된 것을 즉시 알 수 있고 사이트에 좋지 못한 경험을 하지만 웹사이트는 정상적으로 작동하고 있는 상태이다.
커서 페이징이라면 위에서 설명한 대로 커서를 기준으로 하기 때문에 정상적으로 OFFSET 7번부터 조회할 것이다.
4. 효율적인 빅데이터 컨트롤
SQL 내장 OFFSET절이 어떻게 작동하는지 궁금했던 적이 있나? 커서 페이징이 페이징의 가장 효율적인 형태라는 Facebook의 주장을 듣기 전까진 나도 그러지 못했다. 내가 조사해본 결과, 놀라웠다.
오프셋은 단순히 레코드를 조회하기 전에 데이터베이스가 건너 뛰는 레코드의 수다.
즉, 요청한 데이터를 바로 조회하는 것이 아니라, 이전의 데이터를 모두 조회하고 그 ResultSet에서 오프셋을 조건으로 잘라내는 것이다. 오라클 쿼리로 보면 좀더 이해하기 쉽다.
SELECT *
FROM (
SELECT row_number() over (ORDER BY timestamp) rnum
, A.*
FROM table A
ORDER BY timestamp
)
WHERE rnum BETWEEN 6 AND 10
timestamp를 정렬 기준으로 전체 데이터의 행번호를 출력하고 이 번호를 기준으로 잘라내는 것이다.
이건 오프셋 숫자가 커질수록 큰 문제가 된다. 대략 700만개의 레코드 데이터에 대해 커서 및 오프셋 쿼리를 테스트하고 나서야 이해할 수 있었다.
커서 페이징을 사용하면 사용자가 다음 또는 이전을 클릭해 새 페이지를 보기 전까지 0.18초 밖에 걸리지 않았다.
오프셋 페이징을 사용하면 동일한 작업에 13초가 걸렸다. 다시 말해, 각 페이지를 불러올 때마다 13초가 걸린다는 것이다.
컴퓨터에게 13초란 시간은 사람에게 비유하자면 평생동안의 시간과 같다.
대부분의 사용자는 페이지를 부르는 13초를 기다리는 동안 사이트가 망가졌다고 생각할 것이다.
근데 사용자가 700만개의 데이터 중 마지막 페이지를 이용할 확률은 얼마정도 될까? 0%는 아니겠지만, 매우 작을 것이다.
그렇지만 굳이 7백만개의 데이터가 아니더라도, 그래프에서 확인 할 수 있듯이 오프셋 페이징의 시간복잡성 O(N), O(offset+limit) 때문에 오프셋이 커질수록 시간이 증가해 UX는 감소한다. 반면 커서 페이징의 경우는 O(1), O(limit)로 항상 일정하다.
따라서 전보다 페이지 로딩이 느려진 것같은 느낌이 든적이 있다면, 아마도 그건 오프셋 페이징의 비효율성 때문일 것이다.
커서 페이징의 단점
그럼 오프셋 페이징은 이제 사라져야 되는 걸까?
내 생각에는 그럴 필요는 없다. Facebook의 말을 다시 한번 살펴보자.
커서 기반 페이징이 가장 효율적인 방법이며, 가능한 항상 사용되어야 한다.
Facebook은 가능한 경우 커서 페이징을 항상 사용해야 된다고 말하고 있다. 즉, 이말의 뜻은 가능하지 않거나 실용적이지 않은 경우가 분명 존재한다는 것이다.
1. 제한된 정렬 기능
Firstname과 Lastname 응 기준으로 정렬한 테이블 하나를 가정하자. 이 경우는 커서 페이징에 구현에 문제를 발생시킨다. 왜냐하면 커서 페이징 정렬의 요구사항 중 하나는 정렬할 컬럼에 중복된 값이 존재하면 안되고, 순차적이어야 한다는 것이다. 커서 페이징을 사용하려면 "이 레코드 다음 레코드를 조회해줘"라고 할 수 있는 특정 지점을 커서로 지정할 수 있어야 한다.
이런 요구사항 때문에, 대부분의 커서 페이징은 timestamp 컬럼을 기준으로 한다. 왜냐하면 작은 단위의 timestamp는 순차적이고 고유하기 때문이다.
Firstname은 순차적일 수는 있지만 고유하지는 않다. 우리는 김, 박, 최씨를 적어도 100명은 알고 있다. 그래서 이런 경우 커서는 고유한 레코드가 아닌 전체 레코드 집합을 가리킬 수도 있다. 따라서 커서를 구현한 방법에 따라 데이터를 건너 뛰거나 중복될 수 있다.
회원 테이블의 경우 정렬 기준으로 이메일이 더 좋을 수 있다. 고유하고 순차적이라고 볼 수 있기 때문이다.
그러나 요구사항이 Lastname 또는 Firstname으로 정렬하는 것이라면 커서 페이징이 적합하지 않을 수 있다. 이름과 성을 연결하거나 여러 열의 튜플을 사용하여 고유한 열을 만들 수 있지만 이로 인해 커서 페이징이 오프셋 페이징보다 훨씬 느려질 수도 있다. SQL문에서 연결 및 튜플 비교는 모두 시간복잡도 O(N), O(전체 데이터) 를 가지기 때문이다.
실제로, 커서 페이징이 오프셋 페이징보다 첫번째 페이지를 훨씬 느리게 조회했다. 직관적으로 이해되지 않을 수 있지만 Cursor(Concatenated)가 우하향 하는 이유는 레코드가 많아 질수록 SELECT 하는 레코드가 적기 때문이다. 자세히 알아보려면 이 페이지를 참고하라.
2. 조금 까다로운 구현
커서 페이징을 구현하는 것이 엄청 어려운 것은 아니지만, 오프셋 페이징 구현이 매우 쉽기 때문이다. 따라서 시간이 부족하다면 오프셋 페이징이 합리적인 선택이다. 특히 해당 데이터가 실시간 데이터가 아니라면, 굳이 필요하지 않은 부분을 복잡하게 만들 필요는 없다.
커서를 직접 구현해본 입장에서 말하자면, 커서 페이징 구현의 90%는 크게 어렵지 않다. 하지만 오프셋 페이징에 비해 신경써야 할 점이 많다는 것은 분명하다. 즉, 구현하기 어렵진 않지만 일반적으로 오프셋만큼 간단하지는 않다.
따라서 커서 페이징 구현에 더 많은 시간이 요구된다는 것을 기억해라. 그리고 이건 당신과, 회사 그리고 제품 관리자가 의논해야할 일종의 거래다.
3. 무한 스크롤 중독 가능성
사실 엄밀히 말하자면 이것은 커서 페이징의 단점은 아니다. 오프셋 페이징으로도 무한 스크롤을 구현할 수 있기 때문이다. 그러나 커서 페이징은 무한 스크롤을 효과적으로 보조할 수 있는 방법이기 때문에, 실시간 어플리케이션에서 무한 스크롤 구현하는 케이스가 증가했다.
무한 스크롤은 사용자가 한 번에 많은 데이터를 볼 수 있는 편리한 UI 이지만 사용자가 더 이상 의식적으로 버튼을 클릭할 필요가 없다는 점에서 사용자 권한의 일부를 뺏어 왔다고도 볼 수 있다. 이는 사용자가 계속해서 포스트를 보게 만들어 점점 중독되게 만든다.
커서 구현
작은 몇몇 단점들이 있지만 실시간 데이터 처리에 있어서 커서 페이징이 답이다.
이제 왜 Facebook에서 커서 페이징에 대해 강력하게 이야기 했는지 점점 이해가 간다. 그리고 이제 당신의 웹페이지에 커서 페이징을 적용하면 어떨까 생각해볼 시간이다.
만약 커서 페이징이 효율적인 방법이라고 생각한다면, 이 글을 참고하라.