Page와 Slice 구분하기
Spring Data JPA를 사용할 때 페이징 처리를 위해서 일반저긍로 Page와 Slice 객체를 사용한다.
둘 다 페이징 기능을 제공한다는 공통점을 갖고 있으나, 차이가 크다!
우선은 JPA Pageable 페이징 모든 요청 정보를 담는 인터페이스이다.
"몇 번째 페이지를 요청할지? " 와 "페이지당 몇 개의 데이터를 불러올지" 등에 대한 정보들을 캡슐화해준다
Spring Data JPA는 Repository 메서드에 Pageable 타입의 파라미터가 있으면 자동으로 LIMIT, OFFSET, ORDER BY 절을 쿼리에 추가해준다. 즉! 별도의 쿼리 (@Query)를 따로 지정하지 않아도 Spring Data JPA가 알아서 Pageable이 있으면 인식한다!!
(그래서 반환을 Page<T> 객체 있으면 알아서 반환)
public interface PostRepository extends JpaRepository<Post, Long> {
Page<Post> findByCategory(String category, Pageable pageable);
}
위 코드에서 Pageable이 자동으로 인식되어
JPA가 내부적으로 다음과 같은 쿼리를 생성해준다.
SELECT * FROM post
WHERE category = ?
ORDER BY id ASC
LIMIT 10 OFFSET 0;
PageRequest를 생성해서 구현체로 요청을 보내줘야한다. (Pageable은 인터페이스라서 직접 구현이 안돼서 PageRequest 클래스를 사용해서 인스턴스 생성해줘야하는 것임)
Pageable pageable = PageRequest.of(0, 10);
위의 예시 같은 경우에는 첫번째 인자는 page로 0번째 페이지부터 보겠다는 뜻이고,
그 다음 인자는 size로 페이지 당 10개의 데이터를 가져오도록 설계한 것이다.
Page
Page는 페이징된 결과와 함께 전체 데이터의 개수를 포함한 다양한 정보를 제공하는 객체이다.
Spring Data JPA의 Page는 내부적으로 다음과 같은 요소를 기본적으로 제공한다.
- totalElements: 전체 데이터 개수
- totalPages: 전체 페이지 개수
- number: 현재 페이지 번호
- size: 페이지 당 데이터 개수
- first, last, hasNext 등 페이지 이동 관련 플래그
- getContent(): 실제 데이터 리스트
즉, Page는 단순히 데이터 조각을 가져오는 것이 아니라 “전체 페이징 정보”를 함께 다루는 구조이다.
Pageable pageable = PageRequest.of(0, 10);
Page<Post> posts = postRepository.findAll(pageable);
List<Post> content = posts.getContent(); // 실제 데이터
int totalPages = posts.getTotalPages(); // 전체 페이지 수
하지만 Page는 단점도 존재한다.
전체 데이터 개수를 계산하기 위해 select count(*) 쿼리가 추가로 한 번 더 실행된다는 점이다.
이 때문에 데이터가 많거나 쿼리가 복잡한 경우, 성능 저하가 발생할 수 있다.
Slice
Slice는 Page보다 가벼운 페이징 기능을 제공한다.
전체 개수를 계산하지 않고, “다음 페이지가 존재하는지” 여부만 판단한다.
따라서 count(*) 쿼리가 생략되어 성능상 훨씬 유리하다.
Slice는 요청한 pageSize + 1 만큼 데이터를 가져와 hasNext() 여부를 판단한다.
Pageable pageable = PageRequest.of(0, 10);
Slice<Post> posts = postRepository.findSliceBy(pageable);
List<Post> content = posts.getContent(); // 실제 데이터
boolean hasNext = posts.hasNext(); // 다음 페이지 존재 여부
이렇게 하면 전체 데이터 개수를 몰라도,
“다음 페이지가 더 있는지”를 알 수 있다.
이 특성 때문에 무한 스크롤이나 더보기 버튼 같은 기능에서 자주 활용된다.
왜 limit 지정을 하지 않아도 slice는 제대로 작동하는 것일까?
@Query("SELECT p FROM Post p WHERE p.title LIKE %:keyword% ORDER BY p.createdAt DESC")
Slice<Post> findSliceByTitle(@Param("keyword") String keyword, Pageable pageable);
여기서 중요한 점은 쿼리문이 아니라 반환 타입이 Slice이고, Pageable을 파라미터로 전달했다는 것이다.
Spring Data JPA는 Pageable이 전달되면 내부적으로 자동으로 LIMIT, OFFSET 절을 추가한다.
그리고 Slice 타입일 경우, pageSize + 1 만큼 데이터를 조회하여 hasNext()를 계산한다.
countQuery를 실행하지 않기 때문에, Page보다 훨씬 빠르게 작동한다.
그러면 실제 동작할 때에는 다음과 같은 ResponseEntity 를 다음과 같이 담아서 반환한다.
@RestController
@RequestMapping("/api/items")
public class ItemController {
private final ItemService itemService;
@GetMapping("/category/{categoryId}")
public ResponseEntity<?> getItemsByCategory(
@PathVariable("categoryId") int categoryId,
@RequestParam(value = "page", defaultValue = "0") int page
) {
List<ItemDto> items = itemService.getItemsByCategory(categoryId, page);
return ResponseEntity.ok(items);
}
}
다음은 REST Docs에 페이징을 반환한 예시이다.
REST Docs에 Slice를 통한 페이징을 반환할 때 필드를 명시화하기 위한 예시 코드이다.
.andDo(document(
"get-items-by-category",
pathParameters(
parameterWithName("categoryId").description("카테고리 ID")
),
queryParameters(
parameterWithName("page").description("페이지 번호")
),
responseFields(
fieldWithPath("data.content").type(JsonFieldType.ARRAY).description("아이템 목록"),
fieldWithPath("data.size").type(JsonFieldType.NUMBER).description("페이지 크기"),
fieldWithPath("data.number").type(JsonFieldType.NUMBER).description("현재 페이지 번호"),
fieldWithPath("data.first").type(JsonFieldType.BOOLEAN).description("첫 페이지 여부"),
fieldWithPath("data.last").type(JsonFieldType.BOOLEAN).description("마지막 페이지 여부"),
fieldWithPath("data.empty").type(JsonFieldType.BOOLEAN).description("비어있는지 여부")
)
))
반환 데이터는 동일하게 content, size, number, first, last, empty 등의 필드를 포함한다.
다만, 전체 페이지 수나 전체 요소 개수(totalPages, totalElements) 같은 정보가 필요하지 않다면 Page 대신 Slice를 사용할 수 있다.
Slice는 다음 페이지가 존재하는지만 판단할 수 있도록 설계되어, 무한 스크롤 방식과 같이 "다음 데이터가 있는지만 확인하면 되는" 시나리오에 적합하다.
즉, Page와 Slice는 모두 페이징 처리를 위한 유용한 도구이지만, 목적이 다르다.
전체 페이지 정보를 UI에 보여줘야 한다면 Page를 사용하고,
그렇지 않고 단순히 “다음 페이지가 있는지”만 판단하면 된다면 Slice를 사용하는 것이 더 효율적이다.
- Page → 전체 개수 필요할 때
- Slice → 성능과 응답 속도가 중요한 무한 스크롤, 피드, 검색 결과 등
'백엔드 > SpringBoot' 카테고리의 다른 글
| [SpringBoot] UnitTest (1) | 2025.08.27 |
|---|---|
| [SpringBoot] Spring에서 비동기 작업과 스케줄링 - ThreadPoolTaskScheduler 편 (0) | 2025.08.22 |
| [Spring Boot] 동시성 제어하기 (5) | 2025.08.10 |
| [Spring Boot] LazyInitializationException 을 알아보자 (5) | 2025.08.09 |
| [JWT] JWT에서의 로그아웃 (6) | 2025.06.23 |