보편적으로 필요한 갯수를 지정하거나 사용자에게 입력을 받게 되는데 이러한 조건에 맞춰서 데이터를 가져오는 것을 페이징이라고 한다.
페이징 이라는 기술을 사용하지 않으면 엄청난 데이터의 양이 서버 메모리로 적재될 것이고 그리고 서버는 oom이 일어나 결국…
🌟
위 언급한 바와 같이, 페이징이라는 기술이 반드시 필요하게 된다. 이제 페이징 기술들을 한번 보자보자 어디보자 ❗
오프셋 페이징
무엇
✅
오프셋 페이징 방식은 보통 UI가 위 그림처럼 형성되어 있고 내부적으로는 sql query로 limit과 offset이라는 키워드들을 가지고 데이터를 가져오게 된다.
실제 백엔드 쪽에서 사용하는 쿼리를 보자❗
SELECT * FROM ${table_name} ORDER BY CreateAt desc LIMIT ${limit} offset ${offset}
🙋🏻♂️
지금 쿼리의 내용은 ${table_name} 을 갖는 데이터들을 createdAt의 내림차순으로 ${offset} 만큼 건너 뛰어 ${limit} 만큼 데이터를 주세효 🙏🏻 라는 명령어이다.
👀
하지만 여기에서 발생되는 문제가 있다. 과연 무엇일까?
사용자가 보고 있는 페이지에서 다량의 건수의 데이터가 다른 사용자에 의해 삽입되어지고 현재 페이지를 보고 있는 사용자가 다음 페이지로 넘어올 떄 아까 봤던 데이터를 다시 볼게 될 것이다 ❗
띠요오오오옹 ❓👀❓
예를 들어보겠다. [궁금하면 1000원]
👀
3페이지씩 첫번째 페이지를 보고 있다고 가정하자 보이는 순서는 title 순이다.
👀
2페이지로 넘어가려 한다. 그런데 다른 사용자들이 해당 글을 작성하였고 title이 B이고 또 다른 하나는 C이다.
잉? 이게 모야 아까 봣던 거자나~~~~
👀
이런 것들이 한두번이면 괜찮지만 불특정 다수의 사용자들이 언제 넣을지 예측조차 할 수 없는게 현실이다. 그렇기 때문에 어떤 사용자가 페이지를 넘길 때마다 똑같은 것을 볼지 모른다. 해당 사용자는 불쾌감을 느낄 수 있게 될 것이다.
👀
첫번 째 페이지는 B,C라는 title이 들어갈 것이다.
장점
비교적 간편하게 구현하기 용이하다.
문제점
중복 노출
현재 내가 7,8,9가 보이는 2페이지라고 가정해보자
쿼리는 이렇다
select title from ${table_name} order by title asc limit 3 offset 3
그런데 내가 보고 있는 사이 앞에 4,5,6이 들어왔다고 또 한번 가정 하자
그리고 나서 내가 3페이지를 클릭했다.
🙋🏻♂️
이렇게 되면 아까 봤던 7,8,9 가 다시 재출연 되게 될 것이다.
성능 이슈
offset → 데이터를 건너 뛰어야 할 개수이다.
만약 1만개가 있는 상황에서 크기를 20을 요청한다면?
쿼리 : select title from ${table_name} order by title asc limit 20 offset 100000
실제로 offset을 사용하면 10000개를 그냥 뛰어넘는 것이 아닌 이런 쿼리문으로 바뀌어 동작하게 됨으로써 offset만큼 읽어 나가는 것이다.
🙋🏻♂️
이 이후로 사용자 기본 10000개를 읽어야 하게 되는 경우가 발생하고 또한 다른 사용자들이 이런경우가 많게 되면 디비는 음청나게 힘들어질겁니다.
커서 페이징
무엇
✅
커서 패이징 방식은 각 페이지 번호에 데이터들을 불러오는 것이 아닌 자연스럽게 스크롤로 내려가면서 그 때마다 데이터들을 일정 갯수대로 불러오는 방식입니다. 표면적으로 오프셋 방식과 달라보인다. 단순하게 스크롤을 ws 내리면 되므로 현재 다양한 서비스에서 이용되고 있는 기술이다.
장점
우수한 실시간 데이터 처리 능력
오프셋의 데이터 중복 문제를 해결
단점
러닝 커브가 그만큼 존재한다.
쿼리를 매우 잘짜야한다.
중복에 매우 유의 해야 한다.
제한된 정렬 기능
커서 페이징 주의 사항 1 [중복 데이터]
🤓
조금의 가정을 하고 예를 들어보자면, sns 게시글을 최근 발행일자 기준으로 게시글을 노출한다고 가정하면, 아래 그림 같이 정렬되는 것을 알 수 있을 것이다.
id
발행일자
이름
4
2022
b
1
2021
c
2
2021
d
3
2018
e
이떄 현재 가리키는 포인터가 파란색이라고 하면 id가 2번인 로우를 가져올 수 있을지 고민해봐야 한다.
select * from table where 발행일자 < 2021
→ 발행일자보다 빨리 발행된 레코드 가온나 ! 라는 쿼리이다.
이럴떄 id 가 2번은 무시되고 3번을 가져오게 되는 문제가 발생한다.
🤓
해당 상황을 보고 중복에 유의해야 하며 쿼리를 잘짜야 한다는 것이 느껴질 것이다.
🤓
추가적으로 중복된 컬럼명만을 사용해선 안되는 것도 느낄 수 있을 것이다. 결국 중복도가 높은 컬름에서 데이터 유실을 막기 위해서는 고유한 컬럼이 꼭 한가지가 필요하다라고 이해하면 된다.
중복 데이터에 대한 전반적인 해결 로직을 정리해보자면,
우선 커서가 위치한 기반으로 먼저 발행된 데이터들을 긁어온다.
그리고 나서 고유한 값에 대한 (오름차순 또는내림차순) 조건과 커서가 위치한 날짜와 같은 값을 가져온다
만약 상세하게 이해하고 싶으면 이것을 펼치세요 ❗
❗️ Cursor 방식 페이징 사용 시 주의점 [상세 예시]
데이터 유실에 주의 하세요 ‼️
중복이 발생하는 컬럼을 커서로 사용 시 데이터 손실이 발생할 수 있습니다.
처음 id가 18, 17, 14인 데이터를 불러 왔고 커서 값은 ‘2022-08-14 09:53:39’ 가 됩니다.
이제 다음 페이지를 불러올 경우에는 ‘2022-08-14 09:53:39’ 보다 전 시간의 데이터를 불러올 겁니다.
하지만 id가 13인 데이터도 마찬가지로 updatedAt이 ‘2022-08-14 09:53:39’ 이므로 스킵하게 됩니다.
잘못된 페이징으로 데이터 유실이 발생한 것입니다. 이러한 문제를 해결하기 위해서는 중복되더라도 다른 컬럼과 함께 사용해 두번째 규칙을 정해야합니다.
다른 컬럼과 결합해 사용할 때에는 쿼리에 각별히 주의해야합니다. 컬럼을 두 개 이상 사용한다해도 쿼리가 정확하지 않는다면 올바른 페이징을 할 수 없습니다.
아래와 같이 수정일이 최신인 순으로 페이징해 출력하는 상황을 가정합니다. updatedAt이 같을 경우엔 중복되지 않는 값인 pk(id)를 역순으로 정렬 해 쿼리에 조건을 추가 합니다.
쿼리를 잘 짜야합니다! ‼️
3개씩 페이징 한다고 가정 했을 때, 제가 처음 작성했던 쿼리는 다음과 같습니다. 얼핏 봤을 때에는 문제 없이 페이징 되는 듯 해 보였습니다.
⚠️
하지만 이 쿼리에는 문제점이 있었습니다. id는 항상 내림차순이 아닙니다.
일부의 게시물을 Update 하는 상황을 가정해 보겠습니다.
여러 게시물들을 수정해 다음 순서와 같이 데이터가 배치된 경우를 예로 들겠습니다.
자세히 살펴보겠습니다.
첫번째 페이징 후 커서는 updatedAt = 2022-09-04 12:00:00, id = 1을 가르킵니다. 다음 데이터를 불러올 때는 id가 1보다 작은 데이터가 없으므로 다른 데이터들을 조회할 수 없습니다.
기존 쿼리로는 다음 데이터를 불러올 수가 없습니다! 문제가 있어 보이죠? 쿼리는 다음과 같이 작성해야 합니다.
쿼리를 변경해서 다시 자세히 보겠습니다.
처음 3개의 데이터를 불러옵니다 cursor 는 id = 1, updatedAt = 2022-09-04 12:00:00 입니다.
그 다음은updatedAt < 2022-09-04 12:00:00 조건으로 id가 18, 17, 6 … 인 값이 조회되고
2022-09-04 12:00:00:00과 중복되는 데이터가 없으므로 다음으로 넘어갑니다
이제 cursor는 id = 6, updatedAt = 2022-09-04 08:00:00입니다.
다음으로 updatedAt < 2022-09-04 08:00:00 조건으로 3-a 부분이 조회됩니다.
cursor updatedAt보다 작은 데이터들입니다.
id < 6 AND updatedAt = 2022-09-04 08:00:00조건으로 3-b부분이 조회됩니다.
cursor updatedAt과 같지만 id가 더 작은 것 들입니다. 중복 문제가 해결 되었습니다.
🤓
결과적으로 해당 주의사항을 염두해두게 되면 중복 데이터 유실을 방지 할 수 있게 될 것이다.{중복 데이터 컬럼은 언제든지 다를 수 있으므로 고민하셔야 합니다.}
커퍼 페이징 주의사항 2 [제한된 정렬 기능]
Firstname과 Lastname을 기준으로 정렬한 테이블 하나를 가정 해봅니다.
이 경우는 커서 페이징에 구현에 문제를 발생시킨다. 왜냐하면 커서 페이징 정렬의 요구사항 중 하나는 정렬할 컬럼에 중복된 값이 존재하면 안되고, 순차적이어야 한다 는 것 입니다. 커서 페이징을 사용하려면 "**이 레코드** 다음 레코드를 조회해줘"라고 할 수 있는 특정 지점을 커서로 지정할 수 있어야 합니다.
이런 요구사항 때문에, 대부분의 커서 페이징은 timestamp 컬럼을 기준으로 한다. 왜냐하면 작은 단위의 timestamp는 순차적이고 고유하기 때문입니다.
Firstname*은 순차적일 수는 있지만 고유하지는 않습니다. 우리는 김, 박, 최씨를 적어도 100명은 알고 있습니다. 그래서 이런 경우 커서는 고유한 레코드가 아닌 전체 레코드 집합을 가리킬 수도 있습니다. 따라서 커서를 구현한 방법에 따라 데이터를 건너 뛰거나 중복될 수 있습니다.
회원 테이블의 경우 정렬 기준으로 이메일이 더 좋을 수 있습니다. 고유하고 순차적 이라고 볼 수 있기 때문입니다.
그러나 요구사항이 *Lastname* 또는 *Firstname*으로 정렬하는 것이라면 커서 페이징이 적합하지 않을 수 있습니다. 이름과 성을 연결하거나 여러 열의 튜플을 사용하여 고유한 열을 만들 수 있지만 이로 인해 커서 페이징이 오프셋 페이징보다 훨씬 느려질 수도 있습니다. SQL문에서 연결 및 튜플 비교는 모두 시간복잡도 O(N), O(전체 데이터) 를 가지기 때문입니다.
요약 정리 table
페이징 방식
오프셋
페이징
장점
구현 간단
- 중복 데이터 문제 해결
- 처음 부터 스캔 하는 퍼포먼스 문제 해결
단점
- 중복 데이터 노출
- 조회 마다 처음 부터 스캔 (performance)
- 쿼리를 잘 짜야 한다
- 중복된 컬럼 값에 유의
- 제한된 정렬 기능
🍯
오프셋 페이징 그림을 기반으로 백엔드는 커서로 구현할 수 있다는 것을 알면 좋을 것 같습니다 🙂 어디까지나 UI는 사용성이므로 오히려 오프셋 UI가 특정 사용자들이나 특정 서비스에는 더 편할 수 가 있다는점 기억하시면 좋을 것 같습니다. 그럼 25만~
SELECT *
FROM (
SELECT ROWNUM() over (ORDER BY timestamp) rnum
, A.*
FROM table A
ORDER BY timestamp
)
WHERE ROWNUM BETWEEN 6 AND 10sq
SELECT * FROM post WHERE id < ${cursorId} and updatedAt <= ${cursorUpdatedAt}
order by updatedAt desc, id desc limit 3;
# 처음 데이터 3개 불러오기 (마지막 데이터 id = 14, updatedAt = 2022-08-14 09:53:39
SELECT * FROM post order by updatedAt desc, id desc limit 3;
# 다음 페이징
# cursor -> id = 14, updatedAt = 2022-08-14 09:53:39
# 13, 11, 10
SELECT * FROM post WHERE id < 14 and updatedAt <= '2022-08-14 09:53:39'
order by updatedAt desc, id desc limit 3;
# 다음 페이징
# cursor -> id = 10, updatedAt = 2022-08-14 08:53:39
# 8, 7, 9
SELECT * FROM post WHERE id < 10 and updatedAt <= '2022-08-14 08:53:39'
order by updatedAt desc, id desc limit 3;
SELECT *
FROM post
WHERE updatedAt < ${cursorUpdatedAt}
or (id < ${cursorId} and updatedAt == ${cursorUpdatedAt}
ORDER BY updatedAt DESC, id DESC LIMIT 3;