페이지네이션(Pagination)은 많은 데이터를 여러 페이지로 나누어 보여주는 기술입니다. 한 번에 모든 데이터를 불러오는 대신, 데이터의 일부만 불러오도록 하여 성능을 높여주죠!! 효율적인 데이터 조회를 위해 LIMIT과 OFFSET 또는 Cursor 기반 방식이 사용됩니다.
Offset 기반 페이지네이션은 데이터를 일정 크기만큼 가져올 때 사용하는 기법입니다. 이 방식은 LIMIT과 OFFSET을 사용하여 시작 지점과 갖고 싶은 데이터의 개수를 명시합니다.
SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 OFFSET 20;
LIMIT 10: 한 번에 10개의 게시글을 가져옵니다.
OFFSET 20: 첫 번째 게시글부터 20개의 게시글을 건너뛰고 그 다음부터 10개를 가져옵니다.
이 방식은 쿼리에서ORDER BY와 함께 사용하여 내림차순으로 정렬합니다.
LIMIT과 OFFSET을 사용하여 정확한 범위의 데이터를 가져올 수 있습니다!!성능 저하
OFFSET을 사용하면 큰 값일수록 성능이 급격히 떨어질 수 있습니다. 예를 들어, OFFSET 100000을 사용하면 데이터베이스는 100,000개의 레코드를 건너뛰고 데이터를 조회해야 하므로 성능이 저하됩니다.OFFSET이 커질수록 성능이 떨어지는 이유를 설명하고 있습니다.데이터 변경 시 오류:
OFFSET 방식에서는 페이지가 제대로 동작하지 않을 수 있습니다.Seek 기법은 OFFSET을 대신하여 커서 기반의 기준값을 사용하는 방식입니다. Seek 기법은 WHERE 절에서 마지막id나 createdAt과 같은 기준값을 사용하여 성능을 최적화합니다. 예를 들어, WHERE 절에서 createdAt을 기준으로 데이터를 조회하면, 성능이 효율적이고 정확한 페이지네이션이 가능합니다.
Cursor 기반 페이지네이션은 데이터를 "커서" 기준으로 페이지 단위로 조회하는 방식입니다. 커서는 데이터의 마지막 항목을 기준으로 다음 데이터를 조회하는 방식입니다.
성능 최적화
-> 커서 기반 방식은 데이터를 한 번만 스캔하고, 커서 기준으로 데이터를 계속 가져오기 때문에 성능이 뛰어납니다. OFFSET에 비해 데이터가 많아져도 성능 저하가 적습니다.
정확한 페이지네이션
-> 커서는 데이터의 변경이 있어도 정확한 페이지네이션을 제공합니다. 새로 추가된 데이터가 페이지에 영향을 미치지 않으므로 안정적입니다.
무한 스크롤
-> 커서 기반 페이지네이션은 무한 스크롤을 구현하는 데 유리합니다. 페이지 번호 대신 커서를 사용해 끝없이 데이터를 가져올 수 있습니다.
커서 값은 유일해야 합니다. 예를 들어, createdAt이 동일한 두 게시글이 있을 경우, 어떤 데이터를 반환해야 할지 명확하지 않게되겠죠. 이럴 경우 커서를 id와 결합하여 유일성을 보장하는 방법을 사용할 수 있습니다.
createdAt or modifiedAt이 동일한 경우 처리 방법createdAt과 id를 함께 사용하여 유일성을 보장할 수 있습니다.SELECT * FROM posts WHERE created_at < :cursor_created_at OR (created_at = :cursor_created_at AND id < :cursor_id) ORDER BY created_at DESC, id DESC LIMIT :limit;
이렇게 createdAt과 id를 조합하여 커서를 사용하면, 동일한 createdAt을 가진 게시글들에 대해 유일성을 보장할 수 있습니다.
@Query(value = "SELECT * FROM post WHERE id < :cursor ORDER BY id DESC LIMIT :limit", nativeQuery = true)
List<Post> findPostsBeforeCursor(Long cursor, int limit);
id < :cursor: 이 조건은 cursor 값을 기준으로 이후의 데이터만 조회하는 방식입니다. 커서 기반 페이지네이션에서는 이전에 조회된 마지막 항목의 id나 createdAt 값을 커서로 사용하여 그 이후의 데이터를 가져옵니다.
nativeQuery = true가 없어서 데이터 조회가 되지 않음처음에는nativeQuery = true를 설정하지 않아 쿼리가 제대로 실행되지 않았습니다. 이로 인해 데이터가 빈 리스트로 반환되었거나 LIMIT이 적용되지 않았습니다.
=> nativeQuery = true를 추가했습니다.. 몰랐는데 JPA에서 네이티브 SQL 쿼리를 사용하려면 꼭 필요하더라구요. 이를 설정하지 않으면, JPQL로 작성된 쿼리가 실행되며, SQL 문법을 직접 사용할 수 없습니다. (ㅋㅋ;)
처음 페이지에서 커서를 null로 설정하면, id < 0인 데이터를 조회하게 되어 빈 리스트가 반환되었습니다. 이를 해결하기 위해 cursor = Long.MAX_VALUE를 사용하여 첫 번째 페이지에서 모든 게시글을 조회하도록 했습니다.