2 분 소요

데이터의 범위를 정하는건 오프셋방식만 있는것이 아니다.

시리즈

1.커서기반페이징이란?

2.Base64인코딩된 토큰을 사용한 API 통신

3.커서기반페이징 API Request/Response의 BestPractice를 찾아서


배경

언젠가 원인을 알수없는 select count(*)의 무수한 호출로 인해 원인파악 요청을 받은적이 있었다.

내용을 파악해보니,

수 백만의 row를 갖고있는 고객이 자신의 테이블에 select count(*)쿼리를 계속해서 날린다는것이였고, 그 결과로 데이터베이스와 네트워크의 부하가 증가한다는 것이였다.

1.원인

특정 데이터를 조회할 때 count(*)를 계속해서 호출하고 있었고

그 원인은 페이징처리 때문이였다.

페이징처리에 count(*)가 나오는건 당연하지않느냐?할수가 있는데,

중요한건 이 조회API가 UI에 페이지네이션을 위한 API가 아니라, 데이터 수집용 API라는것에 초점이 있다.

1.1.페이징처리란?

페이지네이션을 상상해보면 쉽다.

UI에 페이지네이션을 구현하려면 총 페이지 수와 현재 페이지 데이터가 존재해야한다.

이 내용을 API로부터 받아야한다.

그렇다면 API는 어떻게 이것을 파악할 수 있을까?

사용자의 파라미터 중 limit(한번에 보여줄 데이터 수)과 page(현재페이지)를 받아야한다.

1.2.select count(*), 오프셋 기반 페이징

그러면 이 때,

  1. API는 count(*)쿼리를 발생시켜 총 페이지를 알아내려는 시도를하게된다.
  2. limit n,m을 이용하여 n만큼의 데이터를 조회한 뒤 m만큼만 반환하게된다

이것이 부하의 원인이며 오프셋 기반 페이징기법이다.

1.3.오프셋 기반 페이징의 사이드이펙트

오프셋기반 페이징은 누구나 접하기쉽고, 구현하기도쉽다.

따라서 어디서든 개발할 수 있고, MVP나 애자일방식의 개발방식을 갖고있는 회사라면 빠른 구현을 위해 이 방법을 채택할 수 있다.

데이터 수가 얼마 되지 않을때는 문제가 없지만,

데이터수가많다면?

예를들어 100만개 갖고있는 테이블을 조회하려고할때

오프셋기반 페이징으로 데이터를 조회하려면,

  1. 100만개를 일단 전부 다 카운트를 하고
  2. 그 후 파라미터로 전달받은 limit과 page로 limit n,m을 통해 n만큼을 select한 뒤 m만큼만 반환하여 데이터리스트를 반환한다.

따라서 데이터 수가 늘어날수록 기하급수적으로 느려지게된다.

1.4.오프셋 기반 페이징은 절대악일까?

위의 내용으로, 오프셋 기반 페이징의 단점을 살펴보았지만

절대악은 아니라고생각한다.

일반적인 게시판에 구현되어있는 페이지네이션방식은 오프셋기반 페이징으로만 구현되어야할테니까 말이다.

또한, 데이터수가 얼마없거나 빠른 생산성으로 작업해야할때는 고려할만하다고 생각한다.

2.개선방향

그렇다면 select count(*)가 호출되는 기존 API의 문제점을 어떻게 개선할 수 있을까?

바로 커서기반페이징을 사용하는것이다.

2.1.커서기반페이징

커서기반페이징은 사용자가 보는 마지막 데이터의 값과 limit을 사용하여 페이징한다.

즉 1번~10번까지 데이터를 확인한 사용자에게 ‘다음에는 11번을 요청해’라는 정보를 주는것이다.

그렇다면 1번~10번까지의 데이터를 확인하여 ‘11번을 요청해’라는 정보를 알고있는 사용자는 ‘11번부터 10개줘’를 요청할 수 있는것이다.

그렇다면 select count(*)할 필요 없이 손쉽게 데이터리스트를 반환할 수 있다.

2.2.커서기반페이징의 조건

고유한값으로 정렬되어있어야한다.

만약 고유한값이 아니라면, 페이징에 문제가 생길 수 있다.

예를들어 name으로 커서기반페이징을 하며

사용자가 ‘제이든 다음으로 10개줘’라고 요청했을 때, 아래와같은 데이터들이라면?

1번 제이든 10살

2번 제이든 12살

3번 제이든 14살

4번 에어팟 16살

나는 2번부터 10개를 받길원했지만

4번부터 10개를 받게될것이다.

고유값이아니라면 무엇이든 고유값이 있는것도 order by에 추가되어야한다.

3.개선

데이터의 timestamp로 커서기반페이징을 구현하려고한다.

또한 timestamp가 수정되어 데이터중복이 될 수 있는 상황을 대비하여(이런 상황이 발생되면 안되겠지만) index도 timestamp와 더불어 사용하려고 한다.

용어 치환

  • timestamp : time
  • index : idx

3.1.최초조회 시

select idx,time
from member
order by time desc, idx desc
limit 5; 
# 마지막 idx = 5, 마지막 time = 2023-05-15 17:16:02
  • 기준은 time과 idx의 desc이다.
  • 그렇다면 마지막 idx와 time을 기준으로 다음 조회를 사용할 수 있다.

3.2.다음조회 시

select idx,time
from member
where time <= '2023-05-15 17:16:02' and idx < 5
order by time desc, idx desc
limit 5;
  • 최초 조회 시 얻었던 idx와 time을 사용하여 손쉽게 다음 데이터를 얻을 수 있었다.

4.결과

만약 time이 2023-05-15 17:16:02인 또 다른 데이터가 있다 하더라도,

idx라는 안전장치를 걸어주었기 때문에

중복된 데이터를 건너 뛸 위험성 없이 안전하게 다음 데이터를 가져올 수 있다.

5.Next

위 처럼 개선하기위해서는 idx와 wtime을 파라미터로 받는 API가 있어야한다.

그러나 다음과 같은 대표적인 두 이유로 인해 idx와 wtime을 파라미터로 직접받지않는다.

  • API 클라이언트의 조작방지
  • 신뢰성 보장

그러면 어떻게 이를 해결할 수 있을까?

바로 bas64인코딩을 통해 해결할 수 있다.

다음편에 계속