[BroBean] BroBean 프로젝트 #15 JPA 페이지네이션 적용, Page와 Slice의 차이
📖 BroBean 프로젝트 #15 페이지네이션을 적용하며 Page와 Slice의 차이를 알게 되다
BroBean을 개발하면서 가계부 내역과 알림 내역에도 페이지네이션을 적용하게 되었다.
기존에는 페이지네이션이 필요한 경우 대부분 Page<T>를 사용해서 응답을 내려주고 있었다.
Page<TransactionResponse> transactions = transactionRepository.findAll(
condition,
pageable
);
페이지 번호나 전체 데이터 개수를 같이 내려줘야 하는 경우에는 Page가 자연스럽게 느껴졌기 때문에 별다른 고민 없이 사용하고 있었다.
그런데 이번에 알림 내역의 페이지네이션을 구현하면서 Slice<T>라는 객체를 알게 되었다.
처음에는 단순히 Page와 비슷한 또 하나의 페이지네이션 객체라고 생각했는데, 실제로 살펴보니 전체 데이터 개수를 조회하느냐의 차이가 있었고, 이 차이가 생각보다 꽤 중요한 부분이었다.
특히 어떤 화면에서는 전체 데이터 개수가 필요하지만, 어떤 화면에서는 단순히 "다음 데이터가 있는가?"만 알면 되기 때문에 무조건 Page를 사용할 필요가 없었다.
이번 작업에서는 Page와 Slice의 차이를 정리하고, BroBean에서는 각각 어떤 상황에 적용했는지 기록해보려고 한다.
Page와 Slice
Spring Data에서 제공하는 Page와 Slice는 모두 페이지 단위로 데이터를 조회하기 위한 객체다.
둘의 관계를 먼저 보면 다음과 같다.
public interface Page<T> extends Slice<T> {
...
}
즉, Page는 Slice의 기능을 포함하면서 전체 데이터 개수와 전체 페이지 수에 대한 정보가 추가된 형태라고 볼 수 있다.
간단하게 비교하면 다음과 같다.
| 구분 | Page | Slice |
|---|---|---|
| 현재 페이지 데이터 | O | O |
| 현재 페이지 번호 | O | O |
| 페이지 크기 | O | O |
| 현재 조회된 데이터 개수 | O | O |
| 다음 페이지 존재 여부 | O | O |
| 이전 페이지 존재 여부 | O | O |
| 전체 데이터 개수 | O | X |
| 전체 페이지 수 | O | X |
| 전체 Count 조회 | 일반적으로 필요할 수 있음 | 필요 없음 |
| 주요 용도 | 페이지 번호 기반 UI | 더보기 / 무한 스크롤 |
결국 가장 큰 차이는 전체 데이터 개수를 알고 있느냐이다.
Page
Page는 현재 페이지뿐만 아니라 전체 데이터에 대한 정보까지 제공한다.
예를 들어 데이터가 총 153개이고 한 페이지에 10개씩 보여준다면 다음과 같은 정보를 얻을 수 있다.
현재 페이지: 3
페이지 크기: 10
현재 페이지 데이터: 10개
전체 데이터: 153개
전체 페이지: 16개
다음 페이지 존재: true
이런 정보가 필요한 화면이라면 Page가 적절하다.
특히 프론트에서 다음과 같은 UI를 만들어야 한다면 전체 데이터 개수와 전체 페이지 수가 필요하다.
< 1 2 3 4 5 ... 16 >
또는
전체 가계부 내역 153개
이 경우에는 단순히 현재 페이지의 데이터만 가져오는 것으로는 충분하지 않다.
전체 데이터 개수를 알아야 하기 때문에 Count 조회가 필요하고, Page가 이런 정보를 제공하기에 적합하다.
Slice
반면 Slice는 전체 데이터 개수에는 관심이 없다.
현재 조회한 데이터와 함께 다음 데이터가 존재하는지만 확인한다.
예를 들어 한 번에 10개의 데이터를 보여준다고 해보자.
1페이지
→ 데이터 10개
→ 다음 데이터 있음
2페이지
→ 데이터 10개
→ 다음 데이터 있음
3페이지
→ 데이터 10개
→ 다음 데이터 없음
이 경우 굳이 전체 데이터가 몇 개인지 알 필요가 없다.
프론트에서는 다음과 같이 처리할 수 있다.
알림 10개
[더보기]
그리고 hasNext()가 true라면 더보기 버튼을 보여주고, false라면 더 이상 데이터를 불러오지 않으면 된다.
Slice는 전체 Count를 계산하지 않고 다음 데이터가 존재하는지만 판단하기 때문에 이런 형태의 UI에 적합하다.
Spring Data에서는 이를 판단하기 위해 실제 페이지 크기보다 하나 더 조회하는 방식으로 다음 데이터의 존재 여부를 확인할 수 있다.
예를 들어 페이지 크기가 10이라면 내부적으로 11개를 조회해서 11번째 데이터가 존재하는지 확인하고, 존재한다면 hasNext()를 true로 판단하는 방식이다.
따라서 개념적으로는 다음과 같이 생각할 수 있다.
Page
→ "전체 데이터가 몇 개이고, 전체 페이지가 몇 개인가?"
Slice
→ "현재 데이터 다음에 더 있는가?"
Page를 사용하면 Count 쿼리가 필요한 이유
Page와 Slice의 차이를 알아보면서 가장 관심이 갔던 부분은 Count 쿼리였다.
예를 들어 다음과 같이 Page를 사용한다고 해보자.
Page<TransactionResponse> result =
transactionRepository.findAll(condition, pageable);
가계부 데이터가 다음과 같이 존재한다고 하면,
전체 데이터
153건
현재 페이지
3페이지
페이지 크기
10
현재 페이지의 데이터를 조회하는 것만으로는 전체 데이터가 153건이라는 사실을 알 수 없다.
그래서 일반적으로 데이터 조회와 별개로 전체 개수를 확인하기 위한 Count 쿼리가 필요하다.
개념적으로 보면 다음과 같은 두 가지 작업이 이루어질 수 있다.
SELECT ...
FROM transaction
WHERE ...
LIMIT 10 OFFSET 20;
그리고
SELECT COUNT(*)
FROM transaction
WHERE ...;
첫 번째 쿼리는 실제 데이터를 가져오기 위한 것이고, 두 번째 쿼리는 전체 페이지 수를 계산하기 위한 것이다.
반면 Slice는 전체 데이터 개수가 필요하지 않기 때문에 Count 쿼리를 수행할 필요가 없다.
Slice<NotificationResponse> result =
notificationRepository.findAll(condition, pageable);
결과적으로 데이터 조회에 필요한 쿼리만 수행하면 된다.
물론 실제 Spring Data의 내부 동작에서는 조회 결과나 쿼리 형태에 따라 Count 실행을 최적화하는 경우도 있기 때문에 Page를 사용한다고 항상 무조건 Count 쿼리가 실행된다고 단정할 수는 없다.
다만 기본적인 차이는 Page는 전체 개수 계산을 위한 Count 정보가 필요하고, Slice는 필요하지 않다는 것이다.
BroBean에서는 어떻게 적용했을까?
이번 페이지네이션 작업에서는 모든 화면에 같은 객체를 사용하는 것보다 화면에서 어떤 정보가 필요한지를 기준으로 Page와 Slice를 나누는 것에 초점을 맞췄다.
가계부 내역은 Page
가계부 내역은 전체 데이터 개수와 전체 페이지 정보가 필요한 화면이었다.
예를 들어 사용자가 자신의 가계부를 확인할 때,
가계부 내역 127건
과 같은 전체 데이터 개수를 보여줄 수 있고,
< 1 2 3 4 5 ... >
형태의 페이지 네비게이션을 제공할 수도 있다.
이 경우에는 현재 페이지의 데이터만 알아서는 부족하다.
전체 데이터 개수와 전체 페이지 수가 필요하기 때문에 Page를 사용하는 것이 자연스럽다.
Page<LedgerResponse> result =
ledgerRepository.findAll(condition, pageable);
응답에서도 필요한 정보를 사용할 수 있다.
result.getContent();
result.getTotalElements();
result.getTotalPages();
result.hasNext();
특히 가계부처럼 데이터의 규모를 사용자에게 보여주거나 페이지 번호를 직접 이동하는 UI라면 Page가 적합하다.
알림 내역은 Slice
반면 알림 내역은 조금 달랐다.
알림 화면에서 사용자에게 필요한 정보는 전체 알림이 몇 개인지보다는 다음 알림이 더 있는지 여부였다.
예를 들어 이미 확인한 알림이 300개 있다고 해서,
전체 알림 300개
라는 정보를 반드시 사용자에게 보여줄 필요는 없었다.
알림 목록을 20개씩 조회하고 다음 알림이 존재한다면 계속 불러오면 된다.
알림 20개
↓
더보기
알림 20개 추가
↓
더보기
더 이상 없음
이런 경우에는 전체 Count를 계산할 이유가 크지 않다.
그래서 알림 내역에는 Slice를 적용했다.
Slice<NotificationResponse> result =
notificationRepository.findAll(condition, pageable);
그리고 프론트에서는 hasNext()만 확인하면 된다.
result.getContent();
result.hasNext();
전체 알림이 300개인지 3,000개인지는 중요하지 않고, 현재 조회한 알림 이후에 더 가져올 데이터가 있는지만 알면 되기 때문이다.
결국 중요한 것은 페이지네이션 방식의 선택
이번 작업을 하면서 Page와 Slice의 차이를 알게 된 것보다 더 중요했던 부분은 페이지네이션 객체를 화면의 요구사항에 맞춰 선택해야 한다는 점이었다.
기존에는 페이지네이션이 필요하면 자연스럽게 Page를 사용했다.
하지만 실제로 필요한 정보가 무엇인지 생각해보면 두 화면의 요구사항은 달랐다.
가계부
현재 데이터
+ 전체 데이터 개수
+ 전체 페이지 수
+ 다음 페이지 여부
→ Page
알림
현재 데이터
+ 다음 데이터 존재 여부
→ Slice
결국 Page가 Slice보다 무조건 좋은 것도 아니고, 반대로 Slice가 항상 더 좋은 것도 아니다.
전체 데이터 개수가 필요한 경우에는 Page가 필요하고, 단순히 데이터를 이어서 가져오는 형태라면 Slice만으로 충분하다.
특히 데이터가 많아질수록 전체 Count를 계산하는 비용도 고려할 필요가 있기 때문에, 실제로 전체 개수가 필요한 화면인지 먼저 확인하고 페이지네이션 방식을 결정하는 것이 중요하다는 것을 알게 되었다.
마무리
이번 작업 전까지는 페이지네이션을 구현하면서 Page를 주로 사용했고 Slice라는 선택지가 있다는 것도 크게 신경 쓰지 않았었다.
하지만 이번에 알림 내역을 구현하면서 둘의 차이를 살펴보니 단순히 이름만 다른 객체가 아니라, 전체 데이터 개수를 조회해야 하는지에 따라 데이터 조회 방식과 API가 제공하는 정보 자체가 달라지는 구조라는 것을 알게 되었다.
정리하면 다음과 같다.
Page
→ 전체 데이터 개수가 필요하다.
→ 전체 페이지 수가 필요하다.
→ 페이지 번호 기반 UI에 적합하다.
→ Count 조회가 필요할 수 있다.
Slice
→ 전체 데이터 개수는 필요하지 않다.
→ 다음 데이터가 존재하는지만 알면 된다.
→ 더보기 / 무한 스크롤 등에 적합하다.
→ 전체 Count 조회 없이 페이지네이션을 구성할 수 있다.
BroBean에서는 이 기준에 따라
가계부 내역 → Page
알림 내역 → Slice
로 나누어 적용했다.
페이지네이션은 단순히 데이터를 몇 개씩 잘라서 가져오는 기능이라고 생각했는데, 이번 작업을 통해 어떤 페이지네이션 정보를 API에서 제공해야 하는지까지 고려해서 Page와 Slice를 선택해야 한다는 것을 알게 되었다.
작은 차이처럼 보이지만 데이터 조회 비용과 API 응답의 의미까지 연결되는 부분이라, 앞으로 페이지네이션을 구현할 때는 무조건 Page부터 사용하는 것보다는 해당 화면에서 정말 전체 Count가 필요한지를 먼저 확인해볼 것 같다.
[BroBean] BroBean 프로젝트 #14 순환 참조 오류를 ObjectProvider로 해결하기
BroBean 커플 기능을 구현하던 중 AuthService와 CoupleService 사이에서 발생한 순환 참조 문제와 ObjectProvider를 활용해 의존성 주입 시점을 지연시키는 방법을 정리합니다.
[Nuxt Content] Nuxt Icon 오류 해결과 Custom Collection 기반 오프라인 아이콘 구성
제한된 네트워크 환경에서 발생한 Nuxt Icon 모듈 오류 원인을 분석하고 Custom Collection을 활용해 로컬 아이콘 환경을 구성한 과정 정리
