커서 기반 페이지네이션 구현하기

ZEDY·2023년 10월 14일
0

[백엔드] Spring Boot

목록 보기
17/27

커서 기반 페이지네이션(cursor-based pagination)은 데이터베이스에서 대량의 데이터를 효과적으로 페이징하는 기술 중 하나입니다. 이 기술은 무한 스크롤과 같은 사용자 경험을 구현하는 데 유용합니다. 커서 기반 페이지네이션은 다음과 같이 작동합니다:

  1. 데이터베이스 쿼리 작성: 처음 페이지 로딩 시에는 데이터베이스에서 처음 N개의 아이템을 가져오는 쿼리를 작성합니다. 이 때, 쿼리는 일반적으로 LIMIT 또는 TOP 절과 함께 사용하여 결과 집합의 크기를 제한합니다.

  2. 커서 생성: 처음 데이터를 로드한 후, 마지막으로 로드한 아이템의 고유한 식별자(예: ID)를 기록합니다. 이것이 다음 페이지를 가져올 때 사용되는 커서(cursor) 역할을 합니다.

  3. 다음 페이지 요청: 사용자가 무한 스크롤을 하면, 백엔드에 다음 페이지를 요청합니다. 이때, 이전 페이지에서 기록한 마지막 아이템의 식별자를 함께 보냅니다.

  4. 커서 기반 쿼리 실행: 백엔드는 이전 페이지에서 받은 커서를 사용하여 데이터베이스 쿼리를 작성합니다. 이 쿼리는 이전 페이지에서 마지막 아이템 이후의 아이템들을 가져오는 역할을 합니다.

  5. 결과 반환: 데이터베이스에서 새로운 페이지의 데이터를 가져온 후, 이를 클라이언트에 반환합니다. 클라이언트는 이 결과를 현재 페이지에 추가하여 사용자에게 무한 스크롤 경험을 제공합니다.

이러한 커서 기반 페이지네이션의 장점은 다음과 같습니다:

  • 무한 스크롤과 같은 사용자 경험을 제공할 수 있습니다.
  • 데이터베이스 서버에 과도한 부하를 방지할 수 있습니다.
  • 중간 페이지로 이동하기 쉽습니다.

주의할 점은 커서 값이 고유하고 순서가 있는 필드(예: ID, 타임스탬프)를 기반으로 하여야 한다는 것입니다. 또한, 커서가 변경되면 이전 페이지의 데이터를 검색하는 것이 어렵기 때문에 임의의 페이지로 이동하는 것은 지원되지 않습니다.

마지막으로, 페이지네이션을 구현할 때 클라이언트와 서버 간의 커뮤니케이션 프로토콜을 정의하고, 데이터베이스에서 쿼리를 작성하여 커서 기반 페이지네이션을 구현해야 합니다.


내가 마주한 상황 : 무한스크롤이지만, getAll 처럼 받아왔다.
문제가 되는 상황 : SELECT * FROM lesson 과 같은 쿼리는 enum용도의 테이블이 아니라면 백엔드 선에서 컷해야 합니다. 무조건 LIMIT과 함께 사용하여 페이지네이션 형태로 제공해야 합니다.
개선 방법 : cursor-based pagination 구현


  1. 페이지네이션(Pagination): 페이지네이션은 웹 애플리케이션 또는 웹 사이트에서 대량의 데이터나 긴 목록을 여러 페이지로 나누어 표시하는 기술을 가리킵니다. 대개는 한 페이지에 표시할 수 있는 아이템 수에 제한을 두고, 이러한 목록을 여러 페이지로 분할하여 사용자가 쉽게 탐색하고 관리할 수 있게 합니다.
    서버 입장에서도, 클라이언트 입장에서도 특정한 정렬 기준에 따라 지정된 갯수의 데이터를 가져오는 것이 필요하다. 이걸 페이지네이션이라고 한다.

  2. 페이지네이션의 필요성: 페이지네이션은 다음과 같은 이유로 필요합니다.

    • 성능 개선: 대량의 데이터를 한 번에 로드하는 것은 서버 및 클라이언트의 부담이 될 수 있습니다. 페이지네이션을 사용하면 필요한 데이터만 로드하여 성능을 향상시킬 수 있습니다.
    • 사용자 경험 향상: 사용자가 목록을 여러 페이지로 나누어 볼 수 있으므로 사용자 경험이 향상됩니다.
    • 데이터 관리: 페이지네이션은 데이터를 페이지별로 구분하여 관리할 수 있도록 돕습니다.
  3. 커서 기반 페이지네이션(cursor-based pagination): 커서 기반 페이지네이션은 페이지마다 커서 또는 특정 행의 위치를 사용하여 데이터를 나누는 페이지네이션 기술입니다. 이전 페이지에서 어떤 위치에서 멈췄는지를 가리키는 커서를 사용하여 다음 페이지의 데이터를 가져옵니다. 이는 무한 스크롤과 같은 기능을 구현하는 데 매우 유용합니다.

  4. 다른 유형의 페이지네이션: 페이지네이션에는 다양한 유형이 있으며, 몇 가지 일반적인 유형은 다음과 같습니다.

    • 순차 페이지네이션(Sequential Pagination): 각 페이지는 연속적으로 이전 페이지의 다음 데이터를 표시합니다. 가장 일반적인 페이지네이션 유형 중 하나입니다.
    • 숫자 페이지네이션(Numbered Pagination): 사용자에게 페이지 번호를 보여주고 해당 페이지로 이동할 수 있는 방법을 제공합니다.
    • 스크롤 페이지네이션(Scroll Pagination): 사용자가 스크롤을 내릴 때 자동으로 다음 페이지의 데이터를 로드하는 방식입니다. 무한 스크롤에 사용될 수 있습니다.
    • 커서 기반 페이지네이션(Cursor-Based Pagination): 이미 설명한 것처럼, 이전 페이지의 커서 또는 특정 행의 위치를 사용하여 데이터를 나눕니다.
    • 카테고리 기반 페이지네이션(Category-Based Pagination): 데이터를 카테고리 또는 태그와 같은 카테고리 기준으로 페이지별로 구분합니다.

각 페이지네이션 유형은 특정 사용 사례나 사용자 경험을 위해 선택되며, 애플리케이션의 요구 사항 및 디자인에 따라 다릅니다.


  1. 오프셋 기반 페이지네이션(Offset-Based Pagination):

    • 설명: 오프셋 기반 페이지네이션은 특정 페이지를 선택하고 해당 페이지의 아이템을 검색하기 위해 오프셋(데이터 집합의 시작 지점에서의 위치)을 사용하는 방식입니다. 즉, 페이지 번호와 페이지 크기를 기반으로 몇 번째 아이템부터 가져올지를 결정합니다.
    • 구현 방법: 페이지 번호와 페이지 크기에 따라 SQL 쿼리를 작성할 때 LIMITOFFSET과 같은 SQL 절을 사용하여 원하는 페이지의 데이터를 가져옵니다.
    • 문제점:
      • 성능 문제: 대규모 데이터베이스에서는 오프셋을 크게 설정해야 하므로 쿼리가 상당히 많은 리소스를 소비할 수 있습니다. 특히 오프셋이 큰 경우 데이터베이스가 모든 페이지를 검색하고 건너뛴 후 원하는 페이지의 데이터를 반환해야 하므로 성능 문제가 발생할 수 있습니다.
        즉, offset, limit을 사용한 쿼리를 이용하고 이때 offset 값이 클 때 문제가 발생합니다. (이를 커서 기반 페이지네이션이 해결해줍니다.)
      • 데이터의 변경: 데이터베이스에 새로운 항목이 추가되거나 삭제되면 오프셋이 변경되므로 일관성 유지가 어려울 수 있습니다.
      • 갱신된 데이터: 페이지를 뒤로 돌아가거나 앞으로 이동할 수 없으므로 동일한 쿼리를 반복해서 실행할 때 새로 추가된 데이터나 변경된 데이터를 놓치는 문제가 있을 수 있습니다.
  2. 커서 기반 페이지네이션(Cursor-Based Pagination):

    • 설명: 커서 기반 페이지네이션은 이전 페이지에서 어디까지 데이터를 검색했는지 나타내는 커서 또는 특정 아이템의 위치를 사용하여 데이터를 나누는 방식입니다. 쿼리가 이전 결과를 가리키는 커서를 사용하여 다음 페이지의 데이터를 가져옵니다.
    • 구현 방법: 커서와 페이지 크기를 기반으로 SQL 쿼리를 작성할 때 이전 결과에서부터 커서 이후의 데이터를 검색하는 방식으로 구현합니다. 즉, 사용자에게 응답해준 마지막 데이터의 식별자 값을 커서로 사용합니다.
    • 장점:
      • 성능 개선: 커서 기반 페이지네이션은 오프셋과 달리 페이지 크기와 상관없이 일관된 성능을 제공합니다.
      • 갱신된 데이터 처리: 쿼리 실행 시점에 커서 위치에 해당하는 데이터만을 반환하므로 페이지 네이션 시점에서 변경된 데이터를 처리할 수 있습니다.
    • 문제점:
      • 커서 관리: 커서 값을 관리해야 하므로 약간의 추가 작업이 필요합니다.

예를 들어, 오프셋 기반 페이지네이션은 "첫 번째 페이지에 10개의 항목을 표시"라는 방식으로 데이터를 가져오는 반면, 커서 기반 페이지네이션은 "이전 페이지의 마지막 항목부터 시작해 다음 페이지에 10개의 항목을 표시"하는 방식으로 데이터를 가져옵니다. 커서 기반 페이지네이션은 대량 데이터와 무한 스크롤 기능을 구현하는 데 매우 효과적입니다.


커서 기반 페이지네이션 예시

커서 기반 페이지네이션을 이해하기 위해 "포스트(Post)" 데이터의 리스트를 페이지네이션하는 간단한 예를 들어보겠습니다. 각 포스트는 고유한 ID와 컨텐츠를 가집니다.

  1. 데이터 모델:
public class Post {
    private Long id;
    private String content;

    // 생성자, getter, setter, 등 필수 메서드 구현
}
  1. 커서 기반 페이지네이션을 위한 REST API:
@RestController
@RequestMapping("/posts")
public class PostController {
    @Autowired
    private PostService postService;

    // 페이지네이션을 위한 API 엔드포인트
    @GetMapping("/paginate")
    public ResponseEntity<List<Post>> getPosts(@RequestParam(value = "cursor", required = false) Long cursor, @RequestParam(value = "limit", defaultValue = "10") int limit) {
        List<Post> posts = postService.getPosts(cursor, limit);
        return ResponseEntity.ok(posts);
    }
}
  1. 서비스 레이어:
@Service
public class PostService {
    @Autowired
    private PostRepository postRepository;

    public List<Post> getPosts(Long cursor, int limit) {
        if (cursor == null) {
            // 첫 번째 페이지
            return postRepository.findTopByOrderByIdDesc(limit);
        } else {
            // 이전 페이지의 마지막 포스트 ID부터 시작
            List<Post> posts = postRepository.findNextPage(cursor, limit);
            return posts;
        }
    }
}
  1. 리포지토리:
public interface PostRepository extends JpaRepository<Post, Long> {
    @Query("SELECT p FROM Post p WHERE p.id < :cursor ORDER BY p.id DESC")
    List<Post> findNextPage(@Param("cursor") Long cursor, @Param("limit") int limit);

    List<Post> findTopByOrderByIdDesc(int limit);
}

이 예제에서, /posts/paginate API는 cursor 파라미터를 사용하여 이전 페이지의 마지막 포스트 ID를 지정할 수 있으며, limit 파라미터를 사용하여 한 페이지에 표시할 포스트 수를 지정할 수 있습니다. 첫 번째 페이지를 요청할 때는 cursor를 생략하거나 null로 설정합니다. 이전 페이지의 마지막 포스트 ID를 알고 있으면 그 값을 cursor로 사용하여 다음 페이지를 요청할 수 있습니다.

이런 식으로 커서 기반 페이지네이션을 구현하면 페이지간의 일관성을 유지하면서 대량의 데이터를 효과적으로 처리할 수 있습니다.


참고)
https://www.devjoon.com/41


내 머리가 안좋아서ㅣ,,ㅜ

profile
Spring Boot 백엔드 주니어 개발자

0개의 댓글