티스토리 뷰
들어가기 전에
이 글은 use-the-index-luke 사이트의 no-offset 글을 번역한 글입니다.
원 글이 좀 딱딱한 것 같아서 이해하기 쉽게 번역해보았습니다. 참고부탁드립니다.
왜 offset을 사용하면 안돼?
SQL로 Pagination을 구현하기 위해 구글을 찾다보면 아래와 같은 결과를 자주 볼 수 있다.
SELECT *
FROM table
WHERE {condition}
LIMIT {contents 개수} OFFSET {page number}
offset
은 데이터베이스에서 쿼리의 처음 N개 결과를 건너뛰도록 설정한다. 하지만 데이터베이스는 N개 결과 이후의 row만 받아오는 것이 아니라 모든 row를 디스크로부터 읽어오고, N개의 결과까지 순서대로 진행하며 넘기게 되는 식으로 진행된다.
즉, offset
을 사용하는 것은 많은 행을 읽어들여오고, 이후에 삭제하는 과정을 거친다. 당연하게도 big offset
값은 SQL이던 NoSQL이던 간에 데이터베이스에 많은 부하를 주게 된다.
또한 새로운 row가 추가될 때의 문제점이 존재한다. 만약 특정 페이지에서 다음 페이지로 가는 사이에 새로운 row가 추가되면 어떻게 될까?
offset
이 4라고 가정하자. 새로운 row가 추가되면, 다음 페이지를 조회할 때 이전 페이지와 중복되는 데이터가 조회될 수 있다. 데이터의 중복 유무가 중요하지 않은 일반적인 게시판이면 문제 없을 수 있지만, 결제 등과 같이 데이터가 중복되어서는 안되는 경우 큰 문제로 다가올 수 있다.
이건 비단 데이터베이스 만의 문제라고 볼 수 없다. 여러 프레임워크에서 위와 같은 방식으로 구현해놓은 경우도 많기 때문이다.
참고로 데이터베이스 별로 offset
구문은 다를 수 있다.
offset
,2 parameter limit
,row_number()
,rownum
등으로 사용되어진다.
이러한 방법들의 공통적인 문제는 삭제할(뛰어넘을) 행 수만 제공한다는 것이다. 더 이상의 컨텍스트/정보는 제공하지 않는다.
Offset을 사용하지 않는다면
offset
을 사용하지 않는다면, 어떻게 해야할까?
다시 pagination
을 구현하는 이유에 대해 생각해보면 된다. 우리는 아직 보지 않는 데이터(row)의 정해진 개수만 조회하고 싶은 것이다. 이를 위해선 마지막에 조회한 row id를 사용하면 된다. 즉, 아래처럼 쿼리를 작성해 pagination
을 구현할 수 있다.
- 게시판, 무한 스크롤 등
pagination
이 필요한 곳에서는 정렬된 데이터를 필요로 한다. 그러므로order by
가 필수적이다.
SELECT ...
FROM ...
WHERE ...
AND id < ?last_seen_id
ORDER BY id DESC
FETCH FIRST 10 ROWS ONLY
이러한 방법을 seek method
또는 keyset pagination
이라고 하며 (아래 해당하는 것들이 다 같은 것을 의미), 기존 offset
을 사용한 pagination
보다 성능이 우수하며 위에서 설명한 여러 문제들을 해결할 수 있다.
- SQL seek method
- keyset pagination
- cursor pagination
- no-offset pagination
관련해 아래 사이트들을 참조하자. 여러 비교를 통해 성능 등에서 우수하다는 점을 보여주고 있다.
- https://www.slideshare.net/MarkusWinand/p2d2-pagination-done-the-postgresql-way
- https://vladmihalcea.com/sql-seek-keyset-pagination/
근데 keyset pagination
방식도 단점이 존재한다. 바로 임의의 특정 페이지로 바로 이동이 불가능하다는 것이다. 위 쿼리를 봐서 알겠지만 최근에 조회한 row 이후의 데이터들만 가져오도록 하고 있지, 특정 페이지 번호를 통해 조회하고 있지 않다. 그러므로 비즈니스 로직 상 특정 페이지의 데이터를 조회하는 것이 필요하다 했을 때, keyset pagination
는 적합하지 않다. 무한 스크롤 방식이라면 문제가 되지 않는다.
참고로 원작자는 페이지 번호를 표시해 임의의 페이지로 이동하는 인터페이스가 poor navigation
이라고 말하고 있다. 느낌 상 '그러므로 keyset pagination
를 사용하는 것은 문제가 없어!'라고 말하고 싶은 것 같은데, 근데 게시판 같은 구현에서는 페이지 번호를 사용하는 인터페이스가 필수적이지 않나 싶기도 하다.
However, this is not a problem when using infinite scrolling. Showing page number to click on is a poor navigation interface anyway—IMHO.
그럼에도 사람들은 왜 keyset pagination
보다 offset
을 더 선호하는 걸까? 더 사용하고 있는 걸까?
원작자는 그 이유가 lack of tool support. 즉, 지원되는 도구가 부족하기 때문이라고 말하고 있다. 대부분의 데이터베이스 도구/프레임워크들은 offset
을 활용한 pagination
을 제공하고, keyset pagination
방식을 위한 방법은 제공하지 않고 있다. 그렇기에 이러한 keyset pagination
의 사용이 퍼지기 위해서는 더 다양한 도구/프레임워크에서 지원해줘야 한다.
- 하나의 예를 들어보자면 Spring Data JPA가 있다. Spring Data JPA에서는
Pageable
,Page
객체를 통해 쉽게pagination
구현을 할 수 있도록 지원하고 있다. Database마다 다를 수 있지만 H2 DB를 사용해 테스트해보면,offset
을 사용해pagination
처리를 하고 있는 것을 확인할 수 있다. - 추가로 만약 Spring data jpa을 통해
keyset pagination
를 구현하고 싶다면 이 글을 참고해보자.
결론
만약 임의의 페이지 이동이 필요한 경우가 아니라면 pagination
구현 시 offset
을 사용하지 말고 keyset pagination
을 사용하자. 이 점이 offset
을 사용함으로써 발생하는 여러 문제를 해결할 수 있고 성능 또한 더 우수하다.
추가 (#2022-07-22)
직접 keyset pagination을 구현해보면서 느낀 것인데, 정렬 조건이 복잡한 로직인 상황에서 적용하면 코드가 복잡해져 유지보수성이 떨어지는 것 같다. 구현 시에 메서드 및 클래스 단위로 잘 추출해서 구현하는 것이 중요할 것 같다.
예를 들어, 날짜가 정렬조건인 경우 unique value가 아닐 확률이 높기 때문에 index 같은 id 값을 같이 정렬조건으로 잡아주어야 중복된 또는 생략되는 레코드 없이 Pagination을 구현할 수 있다. 그렇다보면 where 조건절이 커지기 때문에 코드의 핵심적인 부분이 보기 어려워진다.
이 글의 주제와 모순이지만, 만약 복잡한 정렬조건이 구현되어야 한다면 Offset Pagination을 사용하는 것도 나쁘지 않다고 본다 (Trade-off).
References
'Development' 카테고리의 다른 글
Javascript에서의 물음표(?) 사용 (0) | 2023.02.23 |
---|---|
인증(AuthN)과 인가(AuthZ)에 대한 이야기 (0) | 2022.06.12 |
echo와 base64 인코딩 : Kubernetes Secret (1) | 2022.06.10 |
Amazon Linux jdk 17 설치방법 (0) | 2022.01.19 |
Command(커맨드) / Query(쿼리) (0) | 2022.01.18 |
- Total
- Today
- Yesterday
- python
- 로그
- Kubernetes
- Spring
- Intellij
- 하루
- jasync
- hexagonal architecture
- tag
- 클린 아키텍처
- gradle
- MySQL
- WebFlux
- Istio
- container
- 쿠버네티스
- docker
- Algorithm
- boj
- Clean Architecture
- 일상
- Log
- HTTP
- 비동기
- 알고리즘
- Spring boot
- 백준
- k8s
- java
- c++
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |