Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Seminar3] 페이징 처리 #9

Open
yeeun0702 opened this issue Nov 8, 2024 · 0 comments
Open

[Seminar3] 페이징 처리 #9

yeeun0702 opened this issue Nov 8, 2024 · 0 comments

Comments

@yeeun0702
Copy link
Collaborator

yeeun0702 commented Nov 8, 2024

ToDo 페이징 처리에 대해 고민해보세요.

keywords : offset based paging / cursor based paging

  • 동작 원리와 장단점을 고민해보세요.
  • 고민한 내용은 Forum 에 올려주세요.

1️⃣ 페이징이란 ?

페이징은 책 페이지처럼 데이터를 묶음으로 분리하는 과정입니다. 페이징 처리는 데이터를 나누어 보여주는 방식으로, 효율적인 데이터 조회와 전송을 위해 필수적인 개념입니다. 페이징에는 offset 기반 페이징커서 기반 페이징 두 가지 주요 방식이 있습니다.

2️⃣ Offset 기반 페이징 (Offset-Based Paging)

동작 원리

  • offset (=상쇄하다) , 쉽게말해, 지금까지 받아온 데이터의 개수
  • Offset 기반 페이징은 데이터의 시작 위치와 보여줄 데이터의 개수를 지정하여, 특정 페이지의 데이터만 조회하는 방식입니다.
  • SQL에서 LIMITOFFSET 구문을 사용하여 구현할 수 있습니다. 예를 들어, LIMIT 10 OFFSET 30은 30번째 데이터부터 10개를 조회하는 식입니다.

장점

  • 단순하고 직관적: 페이지 번호와 페이지 크기만으로 쉽게 구현 가능하여, 사용자나 개발자 모두 이해하기 쉽습니다.
  • 임의 페이지로의 이동이 용이: 원하는 페이지를 간단히 접근할 수 있어 자유롭게 이동할 수 있습니다.

단점

  • 성능 문제: 데이터가 많아질수록 큰 Offset 값을 처리하는 데 시간이 오래 걸립니다. 특히 큰 테이블에서는 전체 스캔이 필요해질 수 있습니다.
  • 데이터 불일치 가능성: 실시간으로 데이터가 변경될 경우, 페이지가 변동하여 중복된 데이터가 나타나거나 누락되는 현상이 발생할 수 있습니다.

📌 오프셋 기반 페이지네이션의 문제점 예시

오프셋 기반 페이지네이션은 새로운 데이터가 실시간으로 추가되는 상황에서 중복된 데이터를 보거나 데이터를 놓치는 문제가 발생할 수 있습니다. 이를 쉽게 이해할 수 있도록 예시를 들어보겠습니다.


🔎 문제 상황 예시

  • 가정:
    • 사용자가 매우 많은 사이트에서 글이 활발하게 작성되고 있습니다.
    • 초당 평균 20개의 새로운 글이 작성됩니다.
    • 페이지네이션은 오프셋 기반으로 이루어지며, 한 페이지에는 20개의 글이 표시됩니다.

🔎 시나리오 1 : 중복 데이터

  1. 사용자가 1페이지를 조회:
    • 사용자가 첫 번째 페이지를 열 때, 오프셋 기반 페이지네이션을 사용하여 최신 글 20개가 표시됩니다.
    • 예를 들어, 1페이지의 글 목록이 [A1, A2, A3, ..., A20]이라고 가정합니다.
  2. 사용자가 2페이지로 이동:
    • 사용자가 2페이지로 이동하려고 할 때, 그 사이에 20개의 새로운 글(B1, B2, ..., B20)이 추가되었습니다.
    • 오프셋을 기준으로 2페이지를 조회하면, 이전 20개를 건너뛴 이후 20개의 글을 가져오게 됩니다.
    • 따라서 2페이지에는 [A1, A2, ..., A20]이 다시 표시됩니다.
  3. 결과:
    • 사용자는 1페이지에서 봤던 데이터 [A1, A2, ..., A20]을 다시 보게 됩니다.
    • 오프셋이 밀리면서 중복된 데이터를 보게 되는 상황이 발생합니다.

🔎 시나리오 2: 데이터 누락 상황

  1. 사용자가 1페이지를 조회:
    • 사용자가 첫 번째 페이지에서 최신 글 20개 [A1, A2, ..., A20]을 확인합니다.
  2. 2페이지로 이동 중에 20개 이상의 새 글이 추가됨:
    • 사용자가 2페이지로 이동하는 동안, 30개의 새로운 글(B1, B2, ..., B30)이 추가됩니다.
  3. 오프셋으로 인한 데이터 누락 발생:
    • 사용자가 2페이지를 볼 때, 오프셋이 20으로 설정되어 [B1, B2, ..., B20]이 표시되며 중간의 A21~A30은 조회되지 않습니다.
    • 결과적으로 사용자는 중간 데이터(A21~A30)를 놓치게 됩니다.

🔎 요약

오프셋 기반 페이지네이션에서 실시간으로 데이터가 추가되는 환경에서는 다음과 같은 문제가 발생할 수 있습니다:

  1. 중복 데이터: 페이지 이동 시, 이전 페이지에서 봤던 데이터를 다시 보게 되는 문제.
  2. 데이터 누락: 새로운 데이터로 인해 중간 데이터가 밀려 사라지면서 놓치는 문제가 발생.

이 문제를 해결하려면 커서 기반 페이지네이션을 사용하여 마지막으로 본 글을 기준으로 이후 데이터를 조회하는 방식이 더 적합합니다.

📌 Offset 기반 페이징의 SQL 예시

Offset 기반 페이징은 LIMITOFFSET 구문을 사용하여 데이터를 특정 범위로 제한합니다.

  • LIMIT: 조회할 데이터의 최대 개수를 지정합니다.
  • OFFSET: 조회할 데이터의 시작 위치를 지정합니다.

예를 들어, LIMIT 10 OFFSET 3030번째 위치부터 10개의 데이터를 조회하라는 의미입니다.

1. 기본 페이징 쿼리

SELECT *
FROM users
ORDER BY id
LIMIT 10 OFFSET 30;
  • 이 쿼리는 users 테이블에서 id 기준으로 정렬된 데이터 중 31번째부터 40번째까지 10개의 데이터를 가져옵니다.
    • ORDER BY id: id를 기준으로 데이터를 정렬합니다.
    • LIMIT 10: 최대 10개의 행을 가져옵니다.
    • OFFSET 30: 30개의 행을 건너뛰고, 31번째 행부터 조회를 시작합니다.

2. 페이징 파라미터를 동적으로 설정하는 경우

  • 보통 페이지 번호(page)와 페이지 크기(pageSize)를 클라이언트에서 받아와 LIMITOFFSET 값을 계산하여 쿼리를 작성합니다.
  • 예시로 페이지 크기를 10으로 가정하고, 페이지 번호에 따라 조회할 데이터를 조정하는 방법입니다.
-- 페이지 크기와 페이지 번호가 동적으로 들어오는 경우
SELECT *
FROM users
ORDER BY id
LIMIT :pageSize OFFSET (:pageNumber - 1) * :pageSize;
  • :pageSize:pageNumber는 파라미터로 들어오는 값입니다.
  • OFFSET 계산: (:pageNumber - 1) * :pageSize를 통해 각 페이지의 시작 위치를 계산합니다.
    • 예: 페이지 번호가 3이고 페이지 크기가 10인 경우, OFFSET 값은 (3 - 1) * 10 = 20이 됩니다.
    • 이때, 21번째 데이터부터 시작하여 LIMIT 10에 따라 총 10개의 데이터를 조회합니다.

📌 Offset 기반 페이징 구현 예시 (Java + JPA)

  1. 스프링 jpa에 제공하는 Pageable 객체 사용
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findAll(Pageable pageable);
}
  1. 페이징 요청

Pageable 구현체에는 PageRequest라는 객체가 존재합니다.

// 페이지 번호 (0부터 시작)와 페이지 크기 설정
int pageNumber = 2;
int pageSize = 10;
Pageable pageable = PageRequest.of(pageNumber, pageSize);

// 페이징된 결과 조회
Page<User> userPage = userRepository.findAll(pageable);

// 결과 데이터 가져오기
List<User> users = userPage.getContent();

3️⃣ 커서 기반 페이징 (Cursor-Based Paging)

동작 원리

  • Cursor(=데이터를 가리키는 값) , 쉽게 말해, 지금까지 받은 데이터를 표시하는 책갈피
  • 커서 기반 페이징은 마지막으로 조회한 데이터의 고유 식별자(Cursor)를 기준으로 그 다음 데이터를 조회하는 방식입니다.
    • 커서 값: 이전 요청에서 마지막으로 조회된 데이터의 ID나 타임스탬프 같은 고유값을 전달합니다.
    • LIMIT: 가져올 데이터의 개수를 제한하는 LIMIT을 설정합니다.
  • 데이터의 식별자(예: ID)를 기준으로 다음 데이터만 조회하여, 마지막 데이터 다음부터 일정 개수만큼을 가져옵니다. 예를 들어, "ID가 100 이상인 데이터 10개"를 요청하는 식입니다.

장점

  • 고속 처리: 이전 커서 값 이후의 데이터를 바로 조회하기 때문에, 오프셋 스캔보다 빠르고 성능이 좋습니다.
  • 일관된 데이터 제공: 실시간 데이터 변동이 발생해도 커서 위치를 기준으로 조회하므로, 누락이나 중복이 없는 일관된 데이터 조회가 가능합니다.

단점

  • 임의 페이지로 이동이 어려움: 커서를 기준으로 순차적으로 데이터를 불러오므로, 특정 페이지로 이동하는 것이 어렵습니다.
  • 고유 식별자 필요: 커서로 사용할 수 있는 고유 ID 또는 식별자가 필요하며, 복잡한 정렬 기준이 포함될 경우 적용이 까다로울 수 있습니다.

📌 커서 기반 페이징 SQL 예시

커서를 기반으로 데이터를 조회하는 SQL 구문의 예시는 다음과 같습니다. 이 예시에서는 ID를 기준으로 페이징합니다.

  1. 첫 페이지 요청

첫 페이지는 커서 없이, 조회할 데이터의 개수만 지정하여 요청합니다.

SELECT *
FROM users
ORDER BY id ASC
LIMIT 10;
  • users 테이블에서 id 기준으로 정렬된 첫 10개의 데이터를 가져옵니다.
  1. 다음 페이지 요청

첫 페이지 조회 결과에서 마지막으로 조회한 데이터의 id를 커서로 사용하여 이후 데이터를 가져옵니다.

예를 들어, 첫 페이지의 마지막 id10이라고 가정하면, WHERE 절을 사용해 id10보다 큰 데이터만 조회합니다.

SELECT *
FROM users
WHERE id > 10
ORDER BY id ASC
LIMIT 10;
  • id10보다 큰 데이터를 id 오름차순으로 정렬하여 10개만 가져옵니다.

📌 커서 기반 페이징 구현 예시 (Java + JPA)

Java와 Spring Data JPA를 사용하여 커서 기반 페이징을 구현하는 예시입니다. 여기서는 id를 커서로 사용하는 방식입니다.

  1. 커서 기반 메서드 정의

Spring Data JPA에서는 쿼리 메서드를 사용해 커서 기준 조회를 구현할 수 있습니다.

import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {
    // 커서를 기준으로 이후 데이터 조회 (id가 주어진 커서보다 큰 데이터 조회)
    List<User> findByIdGreaterThanOrderByIdAsc(Long cursorId, Pageable pageable);
}
  1. 페이징 요청하기

이제 커서를 기반으로 UserRepository에서 페이징된 데이터를 조회합니다.

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

// 페이지 크기와 커서 ID를 설정
Long cursorId = 10L; // 이전 요청의 마지막 ID 값
int pageSize = 10;
Pageable pageable = PageRequest.of(0, pageSize);

// 커서를 기준으로 다음 데이터 조회
List<User> nextPageUsers = userRepository.findByIdGreaterThanOrderByIdAsc(cursorId, pageable);

// 조회된 데이터에서 마지막 항목의 ID를 가져와 다음 커서로 사용
if (!nextPageUsers.isEmpty()) {
    cursorId = nextPageUsers.get(nextPageUsers.size() - 1).getId();
}
  • findByIdGreaterThanOrderByIdAsc 메서드는 cursorId보다 큰 id를 가진 데이터 10개를 조회합니다. 다음 요청을 위해 조회된 마지막 항목의 ID를 저장하여, 이를 새로운 커서 값으로 사용합니다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant