[Spring/Springboot] Spring Data JPA - Paging/Slice

2024. 12. 23. 00:00CS/Spring

Spring Data JPA의 Paging

  • Spring Data JPA는 페이징을 위해 2가지 객체를 제공 → Page, Slice

Paging

  • 사용자가 어떠한 데이터를 요청했을 때, 전체 데이터 중 일부를 원하는 정렬 방식으로 보여주는 방식
  • 페이징 파라미터
    • page : 페이징 기법이 적용되었을 때, 원하는 페이지
    • size : 해당 페이지에 담을 데이터 개수
    • sort : 정렬 기법
    ⇒ 해당 파라미터들을 Peageable 구현체에 담아서 페이징 설정
  • 계층구조

  • Peageable
    • Pagination을 위한 정보를 저장하는 객체
    • interface로 Pageable의 구현체인 PageRequest 객체를 사용
      • PageRequest 생성자의 파라미터에 page, size, sort를 파라미터로 사용 가능

Pageable 사용 흐름

  • 클라이언트 요청 - Controller - Service - Repository
  • Pageable을 사용해 page, size, sort 파라미터를 컨트롤러, 서비스, 리포지토리 계층에서 처리하는 단계

1. 클라이언트 요청

  • 클라이언트는 필요한 page, size, sort 파라미터를 쿼리 스트링으로 서버에 보냄
GET /reviews?page=1&size=5&sort=score,desc

2. Controller 계층

  • 컨트롤러는 클라이언트의 요청을 받아 Pageable 객체를 생성 → 기본값을 설정하거나 클라이언트 요청값을 활용
@RestController
@RequiredArgsConstructor
@RequestMapping("/reviews")
public class ReviewController {
    private final ReviewService reviewService;

    @GetMapping
    public ApiResponse<Page<ReviewDto>> getReviews(
        @PageableDefault(page = 0, size = 10, sort = "score", direction = Sort.Direction.DESC) Pageable pageable
    ) {
        // 서비스 호출
        Page<ReviewDto> reviews = reviewService.getReviews(pageable);
        // 결과 반환
        return ApiResponse.onSuccess(reviews);
    }
}

 


3. Service 계층

  • 서비스 계층은 컨트롤러에서 전달받은 Pageable을 리포지토리에 전달 → 반환된 데이터를 DTO로 변환하거나 추가 처리를 수행
@Service
@RequiredArgsConstructor
public class ReviewService {
    private final ReviewRepository reviewRepository;

    public Page<ReviewDto> getReviews(Pageable pageable) {
        // Repository 호출
        Page<Review> reviews = reviewRepository.findAll(pageable);
        // 엔티티를 DTO로 변환
        return reviews.map(review -> new ReviewDto(
            review.getId(),
            review.getMember().getName(),
            review.getBody(),
            review.getScore()
        ));
    }
}

4. Repository 계층

  • 리포지토리는 JPA의 findAll(Pageable pageable) 메서드를 활용해 데이터베이스에서 페이지네이션된 데이터를 조회
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
    Page<Review> findAll(Pageable pageable);
}

5. 데이터베이스 조회

  • JPA는 Pageable 정보를 기반으로 SQL 쿼리를 생성하여 요청한 페이지의 데이터를 반환
SELECT * FROM review
ORDER BY score DESC
LIMIT 5 OFFSET 5;

6. DTO 변환 (서비스 계층에서)

  • 데이터베이스에서 조회된 엔티티 데이터를 클라이언트가 필요한 정보만 담은 DTO로 변환
public class ReviewDto {
    private Long id;
    private String memberName;
    private String body;
    private int score;

    public ReviewDto(Long id, String memberName, String body, int score) {
        this.id = id;
        this.memberName = memberName;
        this.body = body;
        this.score = score;
    }
}

7. API 응답

  • 최종적으로 클라이언트가 요청한 페이지 정보와 데이터를 포함한 JSON 형식의 응답을 반환
{
  "status": "success",
  "data": {
    "content": [
      {
        "id": 1,
        "memberName": "John",
        "body": "Great service!",
        "score": 5
      },
      {
        "id": 2,
        "memberName": "Jane",
        "body": "Not bad.",
        "score": 3
      }
    ],
    "pageable": {
      "sort": {
        "sorted": true,
        "unsorted": false,
        "empty": false
      },
      "pageNumber": 1,
      "pageSize": 5,
      "offset": 5,
      "paged": true,
      "unpaged": false
    },
    "totalPages": 2,
    "totalElements": 7,
    "last": false,
    "size": 5,
    "number": 1,
    "sort": {
      "sorted": true,
      "unsorted": false,
      "empty": false
    },
    "numberOfElements": 2,
    "first": false,
    "empty": false
  }
}

  1. 클라이언트 요청: /reviews?page=1&size=5&sort=score,desc
  2. Controller: 요청받아 Pageable 객체 생성 → Service 호출
  3. Service: Pageable을 리포지토리에 전달 → 엔티티를 DTO로 변환
  4. Repository: JPA의 findAll(Pageable pageable) 호출
  5. 데이터베이스 조회: LIMIT, OFFSET 포함한 쿼리 실행
  6. DTO 변환: 필요한 데이터만 포함한 ReviewDto로 변환
  7. 응답 반환: 페이지 정보와 데이터를 JSON으로 반환
  • 이 흐름을 통해 클라이언트는 요청한 페이지 데이터를 정렬 조건에 맞게 받을 수 있음

Pageable 과 PageRequest의 차이

  • Pageable과 PageRequest는 모두 Spring Data JPA에서 페이지네이션과 관련된 기능을 제공 → but, 사용 방식유연성에서 약간의 차이 존재
  • PageRequest는 Pageable의 구현체 중 하나로, 직접적으로 사용하려면 PageRequest.of()를 통해 객체를 생성해야함

차이점

  Pageable PageRequest
생성 방법 Spring MVC에서 자동으로 @PageableDefault 또는 쿼리 스트링으로 생성됨 명시적으로 PageRequest.of()로 생성해야 함
유연성 다양한 Pageable 구현체 사용 가능 단일 구현체 (PageRequest)만 사용 가능
Controller 처리 클라이언트 요청 쿼리 스트링(page, size, sort)을 자동으로 처리 컨트롤러에서 수동으로 파라미터를 읽어 PageRequest를 생성해야 함
장점 Spring MVC 통합 및 편리한 기본값 처리 가능 명시적으로 페이지네이션 값을 설정할 때 더 명확하게 동작

PageRequest로 처리하는 방법

  • PageRequest는 Pageable 인터페이스를 구현하므로, 리포지토리와 서비스 계층은 동일하게 사용
  • 그러나 컨트롤러 계층에서 명시적으로 파라미터를 받아 PageRequest를 생성해야 함
  • Controller 계층 : 클라이언트 요청에서 페이지네이션 관련 파라미터를 받아 수동으로 PageRequest를 생성
@RestController
@RequiredArgsConstructor
@RequestMapping("/reviews")
public class ReviewController {
    private final ReviewService reviewService;

    @GetMapping
    public ApiResponse<Page<ReviewDto>> getReviews(
        @RequestParam(defaultValue = "0") int page,
        @RequestParam(defaultValue = "10") int size,
        @RequestParam(defaultValue = "score,desc") String sort
    ) {
        // PageRequest 생성
        String[] sortParams = sort.split(",");
        Sort sortObject = Sort.by(Sort.Direction.fromString(sortParams[1]), sortParams[0]);
        PageRequest pageRequest = PageRequest.of(page, size, sortObject);

        // 서비스 호출
        Page<ReviewDto> reviews = reviewService.getReviews(pageRequest);
        return ApiResponse.onSuccess(reviews);
    }
}
  • Service 계층 : PageRequest는 Pageable을 구현하므로, 기존의 Pageable과 동일하게 처리가능
@Service
@RequiredArgsConstructor
public class ReviewService {
    private final ReviewRepository reviewRepository;

    public Page<ReviewDto> getReviews(PageRequest pageRequest) {
        // Repository 호출
        Page<Review> reviews = reviewRepository.findAll(pageRequest);
        return reviews.map(review -> new ReviewDto(
            review.getId(),
            review.getMember().getName(),
            review.getBody(),
            review.getScore()
        ));
    }
}
  • Repository 계층 : 리포지토리 계층은 Pageable을 사용하므로 기존 코드와 동일하게 작성
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
    Page<Review> findAll(Pageable pageable);
}

PageRequest 사용 시 변경된 흐름

  1. 클라이언트 요청: /reviews?page=1&size=5&sort=score,asc
  2. Controller
    • @RequestParam으로 page, size, sort 파라미터 수동 수집
    • PageRequest 생성: PageRequest.of(page, size, Sort.by("score").ascending())
  3. Service
    • PageRequest를 리포지토리에 전달
    • 반환된 데이터를 DTO로 변환
  4. Repository : JPA에서 Pageable로 처리
  5. 데이터베이스 쿼리 : LIMIT와 OFFSET을 포함한 쿼리 생성
  6. 응답 반환 : 페이지 정보와 데이터를 JSON으로 반환

Pageable vs PageRequest의 선택 기준

  • Pageable 사용
    • Spring MVC 통합이 필요한 경우 (@PageableDefault, 쿼리 스트링 자동 매핑)
    • 간단한 구현과 기본값 처리가 중요할 때
    ⇒ Spring MVC와 통합을 통해 더 편리한 기본값 처리 및 쿼리 매핑 지원
  • PageRequest 사용
    • 파라미터를 명시적으로 다룰 필요가 있을 때
    • Spring MVC 이외의 환경에서도 동일한 방식으로 동작해야 할 때
    • 커스텀 파라미터 처리 및 검증이 필요할 때
    ⇒ 명시적으로 설정해야하므로 코드의 가독성과 명확성을 높이는데 유리함

Slice

1. Page

  • Page는 전체 데이터에 대한 페이지 정보를 제공하며, 총 페이지 수, 전체 요소 수 등을 포함한 페이지네이션 메타데이터를 제공
  • 주요 특징
    • 메타데이터 포함: 총 페이지 수, 전체 요소 수, 현재 페이지 등이 포함됨
    • DB 추가 쿼리 발생: 총 데이터 개수(totalElements)를 계산하기 위해 별도의 COUNT 쿼리가 실행됨
    • 완전한 페이지 정보 제공: 페이징된 데이터와 함께 전체 데이터의 크기를 알 수 있음
  • 메서드 예시
int getTotalPages();

/**
 * Returns the total amount of elements.
 *
 * @return the total amount of elements
 */
long getTotalElements();
  • getTotalPages() - 총 페이지 수
  • getTotalElements() - 전체 데이터 개수
  • getContent() - 현재 페이지 데이터
  • hasNext() - 다음 페이지가 있는지 여부;
  • Page 사용 예시
  • 리포지토리 메서드
Page<Review> findAll(Pageable pageable);
  • 컨트롤러
public ApiResponse<Page<ReviewDto>> getReviews(@PageableDefault Pageable pageable) {
    Page<Review> reviews = reviewRepository.findAll(pageable);
    return ApiResponse.onSuccess(reviews.map(this::convertToDto));
}
  • 클라이언트에게 전체 데이터 개수와 총 페이지 수를 포함한 응답을 제공

2. Slice

  • Page는 Slice와 상속관계
public interface Page<T> extends Slice<T>
  • Slice가 가진 메서드 Page도 사용가능
  • [차이점] Page는 조회 쿼리 이후 전체 데이터 개수를 조회하는 쿼리가 한번 더 실행됨

 

  • Slice는 다음 페이지가 존재하는지 여부만 확인하며, 전체 데이터의 개수나 총 페이지 수에 대한 정보는 제공X
  • 주요 특징
    • 메타데이터 없음: 전체 데이터 개수와 총 페이지 수를 알 수 없음
    • DB 효율적 쿼리: 추가적인 COUNT 쿼리를 실행하지 않으므로 성능이 더 좋음
    • 가벼운 페이지 정보 제공: 현재 페이지 데이터와 다음 페이지 존재 여부만 확인 가능
  • 인터페이스 메서드 예시
public interface Slice<T> extends Streamable<T> {

    int getNumber();
    int getSize();
    int getNumberOfElements();
    List<T> getContent();
    boolean hasContent();
    
    Sort getSort();
    
    boolean isFirst();
    boolean isLast();
    
    boolean hasNext();
    boolean hasPrevious();
    
    default Pageable getPageable() {
      return PageRequest.of(getNumber(), getSize(), getSort());
    }
    
    Pageable nextPageable();
    Pageable previousPageable();
    
    default Pageable nextOrLastPageable() {
      return hasNext() ? nextPageable() : getPageable();
    }

		default Pageable previousOrFirstPageable() {
      return hasPrevious() ? previousPageable() : getPageable();
    }
}
  • getContent() - 현재 페이지 데이터
  • hasNext() - 다음 페이지가 있는지 여부
  • getTotalPages() 및 getTotalElements()는 없음
  • Slice 사용 예시
  • 리포지토리 메서드
Slice<Review> findAllBy(Pageable pageable);

 

  • 컨트롤러
public ApiResponse<Slice<ReviewDto>> getReviews(@PageableDefault Pageable pageable) {
    Slice<Review> reviews = reviewRepository.findAllBy(pageable);
    return ApiResponse.onSuccess(reviews.map(this::convertToDto));
}

 

  • 클라이언트에게는 현재 페이지 데이터와 다음 페이지 존재 여부만 반환됩니다. 더 효율적이지만, 전체 데이터 크기를 알 수 없음

3. Page와 Slice의 차이점

  Page Slice
전체 데이터 크기 제공 (getTotalElements()) 제공하지 않음
총 페이지 수 제공 (getTotalPages()) 제공하지 않음
DB 추가 쿼리 추가적으로 COUNT 쿼리를 실행 실행하지 않음
다음 페이지 존재 여부 hasNext() 제공 hasNext() 제공
사용 목적 전체 데이터를 포함한 상세 페이지 정보 간단한 페이지네이션 또는 스트리밍

4. 적합한 사용 상황상황

  Page 사용 Slice 사용
총 데이터 크기나 총 페이지 수 필요 사용 (getTotalPages, getTotalElements) 적합하지 않음
성능이 중요하고 총 데이터가 불필요 성능에 부담 (COUNT 쿼리 발생) 효율적 (추가 쿼리 없음)
데이터 스트리밍 불필요한 메타데이터를 포함 간단하고 빠른 응답