Page, Slice, List 선택 기준

전체 개수를 계산하는 Page, 다음 페이지 여부만 확인하는 Slice, 단순 목록 List의 선택 기준

Spring Data JPA는 조회 결과를 Page, Slice, List로 반환할 수 있다. 셋 모두 목록을 보여 주지만 데이터베이스에서 실행하는 쿼리와 클라이언트가 알아야 하는 정보가 다르다. 모든 목록을 Page로 반환하면 편해 보이지만, 전체 개수가 필요하지 않은 화면까지 count 쿼리를 실행하게 된다.

반환 타입별 차이

반환 타입 제공 정보 추가 count 쿼리 적합한 화면
Page 현재 목록, 전체 개수, 전체 페이지 발생 가능 페이지 번호와 전체 개수 표시
Slice 현재 목록, 다음 페이지 존재 여부 없음 더보기, 무한 스크롤
List 조회된 목록만 없음 작은 고정 목록, 전체 조회

Page는 전체 페이지 수를 계산하기 위해 count 쿼리를 실행할 수 있다. 데이터가 많거나 조인이 복잡한 목록에서는 count 쿼리가 본 조회보다 느릴 수 있다. “전체 몇 건”이 화면에 꼭 필요한지 먼저 확인한다.

Page는 0부터 시작한다

Spring Data의 Pageable과 Page는 페이지 번호가 0부터 시작한다. 사용자 화면이 1페이지부터 시작한다면 API 경계에서 한 번만 변환하고, 내부 코드는 0 기반을 유지하는 편이 안전하다.

public record PageRequestDto(
    @Min(1) int page,
    @Min(1) @Max(100) int size,
    String sort
) {
    public Pageable toPageable() {
        return PageRequest.of(
            page - 1,
            size,
            Sort.by(Sort.Direction.DESC, sortOrDefault())
        );
    }

    private String sortOrDefault() {
        return sort == null || sort.isBlank() ? "createdAt" : sort;
    }
}

페이지 번호를 서비스 곳곳에서 -1 처리하면 음수 페이지와 경계 오류가 섞인다. 요청 DTO에서 양수 검증과 허용 가능한 정렬 필드 목록을 함께 관리한다. 정렬 컬럼을 사용자 입력 그대로 SQL에 넣지 말고 화이트리스트로 제한한다.

Slice는 다음 페이지를 어떻게 아는가

Slice는 전체 개수 대신 다음 페이지가 있는지만 알려 준다. 구현체는 요청 크기보다 하나 많은 행을 조회해 hasNext를 판단한 뒤 마지막 행을 잘라낼 수 있다. 무한 스크롤에서는 전체 count보다 다음 데이터 존재 여부가 중요하므로 이 방식이 더 적합하다.

Slice<ArticleSummary> findByCategory(
    String category,
    Pageable pageable
);

offset 기반 페이지는 페이지 번호가 커질수록 앞의 행을 건너뛰는 비용이 커질 수 있다. 수천만 건의 목록이나 실시간 데이터라면 마지막으로 본 createdAt과 id를 커서로 보내는 keyset pagination을 검토한다. 이때 정렬 기준이 유일하도록 두 컬럼을 함께 비교해야 중복과 누락을 줄일 수 있다.

API 응답을 직접 감싸기

Spring의 Page를 그대로 JSON으로 노출하면 내부 Pageable 구조까지 응답에 포함될 수 있다. 필요한 필드만 응답 DTO로 변환하면 API 계약을 안정적으로 유지할 수 있다.

public record PageResponse<T>(
    List<T> content,
    int page,
    int size,
    long totalElements,
    boolean last
) {
    static <T> PageResponse<T> from(Page<T> page) {
        return new PageResponse<>(
            page.getContent(),
            page.getNumber() + 1,
            page.getSize(),
            page.getTotalElements(),
            page.isLast()
        );
    }
}

Slice 응답에는 hasNext와 다음 커서만 제공하고, List 응답에는 페이지 메타데이터를 넣지 않는다. 화면에 쓰지 않는 메타데이터를 매번 계산하고 전송하면 데이터베이스와 네트워크 비용만 늘어난다.

선택 기준은 간단하다. 전체 개수와 페이지 번호가 필요하면 Page, 다음 목록만 필요하면 Slice, 페이징이 필요 없는 작은 결과면 List를 사용한다. 화면의 탐색 방식과 데이터 규모를 기준으로 반환 타입을 결정하면 불필요한 count 쿼리와 페이지 번호 오류를 함께 줄일 수 있다.

함께 읽기:

원문 기록: Spring Day 29: 페이지 반환 타입