먼저 페이징을 위해선 메서드에서 Pageable을 인자로 받아야 한다.
Pageable은 인터페이스로 페이징을 위한 정보를 저장하는데 대표적인 구현체인 PageRequest의 생성자를 보면 다음과 같다.
protected PageRequest(int page, int size, Sort sort) {
super(page, size);
Assert.notNull(sort, "Sort must not be null");
this.sort = sort;
}
- page : 페이지 번호 (0부터 시작)
- size : 해당 페이지의 데이터의 개수
- sort : 정렬 기준
Spring Data JPA는 페이징을 처리할 때 컨트롤러에서 파라미터로 인터페이스인 Pageable을 받을 수 있다.
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
Page<Member> page = memberRepository.findAll(pageable);
return page;
}
위와 같이 사용하면 파라미터로 넘어온 정보를 가지고 PageRequest 객체를 생성해준다.
위에서 여기서 반환 타입을 살펴보면 Page로 반환되는 것을 알 수 있다.
JPA는 페이징을 위해 2가지 객체를 제공한다.
1. Page
추가 count 쿼리 결과를 포함하는 페이징 기법을 의미한다.
Slice를 상속받아 Slice의 모든 메서드를 사용할 수 있고 추가로 전체 페이지 수, 전체 데이터 수를 가져오는 메서드가 있다.
따라서 조회 쿼리 이후에 전체 데이터 개수를 조회하는 count 쿼리가 한 번 더 발생한다.
(참고로 전체를 조회하는 count 쿼리는 매우 무겁다.)
public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
//Page<Member> findByUsername(String name, Pageable pageable);
만약 복잡한 sql의 경우 count 쿼리를 분리할 수 있다.
카운트 쿼리 분리할 때 데이터는 left join, 카운트는 left join 안해도 된다.
@Query(value = "select m from Member m",
countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);
2. Slice
추가 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(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable(); //다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
//Slice<Member> findByUsername(String name, Pageable pageable);
즉 Slice는 전체 데이터 개수는 알 수 없으며 이전, 다음 Slice의 존재 여부만 확인 가능하다.
→ 모바일에서 무한 스크롤 구현하는 경우 유용. Page에 비해 쿼리가 하나 덜 날아가므로 데이터 양이 많을수록 Slice를 사용하는 것이 유리하나 전체 페이지 개수나 데이터 개수가 필요한 웹 게시판 같은 경우 Page가 유용하다.
페이지 사이즈 지정
페이지 개수의 경우 글로벌 설정이나 개별 설정을 통해 지정할 수 있다.
- 글로벌 설정
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
- 개별 설정
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username", direction = Sort.Direction.DESC) Pageable pageable) {
...
}
페이징 정보가 여러 개일 때
파라미터로 페이징 정보가 여러 개 넘어올 때 접두사로 구분이 가능하다.
@Qualifier(접두사명) → {접두사명}_xxx로 파라미터로 넘어올 때 해당 pageable이 적용
예시) /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable ... ) {
...
}
Page → DTO
Page는 map() 메서드를 통해 내부 데이터를 다른 것으로 변경 가능하다.
Page<Entity> page = ...
Page<Dto> pageToDto = page.map(x -> new Dto(...));
Page 1부터 시작
Spring Data JPA는 페이징 처리 시에 Page를 0부터 시작한다.
1부터 시작하려면 어떻게 해야할까?
1. spring.data.web.pageable.one-indexed-parameters: true
spring:
data:
web:
pageable:
one-indexed-parameters: true
위 방법은 web 컨트롤러에서 받는 page 파라미터를 -1 처리를 한다.
하지만 응답으로 받은 Page, Slice 객체를 보면 내부 데이터는 0으로 반환한다는 한계가 있다.
{
"content": [
...
],
"pageable": {
"offset": 0,
"pageSize": 10,
"pageNumber": 0 // 0 인덱스
},
"number": 0, // 0 인덱스
"empty": false
}
2. Pageable, Page를 파라미터, 응답으로 사용하지 않고, 직접 클래스를 정의한다.
- 그리고 직접 PageRequest를 생성해서 리포지토리에 넘긴다.
- 응답의 경우도 Page 대신에 직접 정의해서 제공한다.
- + 여러 글을 찾아보니 Spring MVC의 핸들러 메서드에서 페이지네이션을 처리하는 방식에 대한 사용자 정의 설정을 제공하는 역할인 PageableHandlerMethodArgumentResolverCustomizer를 빈으로 등록하는 방법이 있었다.
@Configuration
public class WebConfig {
@Bean
public PageableHandlerMethodArgumentResolverCustomizer customizer() {
return pageableResolver -> {
pageableResolver.setOneIndexedParameters(true); // page 요청이 1로 오면 0으로 인식
pageableResolver.setMaxPageSize(500); // 최대 요청 가능한 size
pageableResolver.setFallbackPageable(PageRequest.of(0, 10)); // page 정보가 없을 때 기본 페이지 설정
};
}
}
GET - http://localhost:8080/api/posts?page=1 요청 시 아래와 같이 page.number = 0으로 인식되는 것을 확인 가능하다.
{
"isSuccess": true,
"code": "200OK",
"message": "성공입니다.",
"result": {
"posts": {
"content": [
{
"id": 1,
"title": "title",
"content": "content",
},
{
"id": 2,
"title": "title",
"content": "content",
},
],
"page": {
"size": 10,
"number": 0,
"totalElements": 3,
"totalPages": 1
}
}
}
}
참고
실전! 스프링 데이터 JPA 강의 | 김영한 - 인프런
김영한 | 스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제
www.inflearn.com
'JPA' 카테고리의 다른 글
[Spring Data JPA] @EntityGraph (0) | 2024.12.30 |
---|---|
[Spring Data JPA] @Modifying (0) | 2024.12.30 |
[JPA] OSIV(Open Session In View) (1) | 2024.12.29 |
[JPA] JPQL (0) | 2024.12.29 |
[JPA] 데이터 타입 (0) | 2024.12.29 |