[Spring/Springboot] Spring Data JPA - Paging/Slice
2024. 12. 23. 00:00ㆍCS/Spring
Spring Data JPA의 Paging
- Spring Data JPA는 페이징을 위해 2가지 객체를 제공 → Page, Slice
Paging
- 사용자가 어떠한 데이터를 요청했을 때, 전체 데이터 중 일부를 원하는 정렬 방식으로 보여주는 방식
- 페이징 파라미터
- page : 페이징 기법이 적용되었을 때, 원하는 페이지
- size : 해당 페이지에 담을 데이터 개수
- sort : 정렬 기법
- 계층구조
- 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
}
}
- 클라이언트 요청: /reviews?page=1&size=5&sort=score,desc
- Controller: 요청받아 Pageable 객체 생성 → Service 호출
- Service: Pageable을 리포지토리에 전달 → 엔티티를 DTO로 변환
- Repository: JPA의 findAll(Pageable pageable) 호출
- 데이터베이스 조회: LIMIT, OFFSET 포함한 쿼리 실행
- DTO 변환: 필요한 데이터만 포함한 ReviewDto로 변환
- 응답 반환: 페이지 정보와 데이터를 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 사용 시 변경된 흐름
- 클라이언트 요청: /reviews?page=1&size=5&sort=score,asc
- Controller
- @RequestParam으로 page, size, sort 파라미터 수동 수집
- PageRequest 생성: PageRequest.of(page, size, Sort.by("score").ascending())
- Service
- PageRequest를 리포지토리에 전달
- 반환된 데이터를 DTO로 변환
- Repository : JPA에서 Pageable로 처리
- 데이터베이스 쿼리 : LIMIT와 OFFSET을 포함한 쿼리 생성
- 응답 반환 : 페이지 정보와 데이터를 JSON으로 반환
Pageable vs PageRequest의 선택 기준
- Pageable 사용
- Spring MVC 통합이 필요한 경우 (@PageableDefault, 쿼리 스트링 자동 매핑)
- 간단한 구현과 기본값 처리가 중요할 때
- 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 쿼리 발생) | 효율적 (추가 쿼리 없음) |
데이터 스트리밍 | 불필요한 메타데이터를 포함 | 간단하고 빠른 응답 |
'CS > Spring' 카테고리의 다른 글
[Spring/Springboot] @Valid 란? (0) | 2024.12.23 |
---|---|
[Spring/Springboot] @RestControllerAdvice/API 응답 통일/에러 핸들러 (0) | 2024.12.22 |
[Spring/Springboot] N+1 문제 해결 방안 - Batch Size/2차 캐시/Subselect Fetching/DTO (0) | 2024.12.22 |
[Spring/Springboot] QueryDSL이란? (0) | 2024.12.22 |
[Spring/Springboot] @EntityGraph (0) | 2024.12.22 |