JPA/SpringDataJPA
[SpringDataJPA] 페이징 (Paging)
걸어가는 신사
2022. 6. 3. 19:33
Spring Data JPA에서는 Query Method에 페이징과 정렬 기능을 사용할 수 있도록 기능을 제공한다.
Example) JpaRepository의 구현체 Class인 SimpleJpaRepository
// SimpleJpaRepository
public List<T> findAll(Sort sort) {
return this.getQuery((Specification)null, (Sort)sort).getResultList();
}
public Page<T> findAll(Pageable pageable) {
return (Page)(isUnpaged(pageable) ? new PageImpl(this.findAll()) : this.findAll((Specification)null, pageable));
}
1. 페이징, 정렬 파라미터
- org.springframework.data.domain.Sort : 정렬 기능
- org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
public interface Pageable {...}
- Pageable은 인터페이스이다.
(1) AbstractPageRequest
- Pageable을 implements 하는 추상 클래스
public abstract class AbstractPageRequest implements Pageable, Serializable {
private static final longserialVersionUID= 1232825578694716871L;
private final int page;
private final int size;
}
- page, size 필드가 선언되어 있다.
(2) PageRequest (Pageable 구현체)
- AbstractPageRequest를 상속받는 대표적인 Pageable의 구현체
public class PageRequest extends AbstractPageRequest {
private static final longserialVersionUID= -4541509938956089562L;
private final Sort sort;
protected PageRequest(int page, int size, Sort sort) {
super(page, size);
Assert.notNull(sort, "Sort must not be null!");
this.sort = sort;
}
public static PageRequest of(int page, int size) {
returnof(page, size, Sort.unsorted());
}
public static PageRequest of(int page, int size, Sort sort) {
return new PageRequest(page, size, sort);
}
public static PageRequest of(int page, int size, Direction direction, String... properties) {
returnof(page, size, Sort.by(direction, properties));
}
public static PageRequest ofSize(int pageSize) {
return PageRequest.of(0, pageSize);
}
...
}
- 세 개의 필드 page, size, sort
- Pageable은 인터페이스이다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageReqeust 객체를 사용한다.
- PageRequest에는 여러 개의 Static 팩토리 메서드가 존재한다.
- 첫번째 파라미터 : 현제 페이지 (page)
- 두번째 파라미터 : 조회할 데이터 수 (size)
- 세번째 파라미터 : 정렬 정보 (sort)
2. 반환 타입
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Pageable pageable);
- Page, Slice, List 3가지의 반환 타입이 있다.
(1) Page
- org.springframework.data.domain.Page
- 추가 count 쿼리 결과를 포함하는 페이징
- ex) 총 게시물(post)의 갯수
- 페이지는 0부터 시작한다.
Page 인터페이스
public interface Page<T> extends Slice<T> { ... }
- Slice 인터페이스를 상속한다.
Page 인터페이스 구현체 Class인 PageImpl
public class PageImpl<T> extends Chunk<T> implements Page<T> {
private static final longserialVersionUID= 867755909294344406L;
private final long total;
public PageImpl(List<T> content, Pageable pageable, long total) {
super(content, pageable);
this.total = (Long)pageable.toOptional().filter((it) -> {
return !content.isEmpty();
}).filter((it) -> {
return it.getOffset() + (long)it.getPageSize() > total;
}).map((it) -> {
return it.getOffset() + (long)content.size();
}).orElse(total);
}
}
- total이라는 필드가 있다. (count 쿼리를 통해서 값이 주입된다.)
(2) Slice
- org.springframework.data.domain.Slice
- 추가 count 쿼리 없이 다음 페이지만 확인 가능 (내부적으로 limit + 1 조회)
Slice 인터페이스
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 인터페이스 구현체 Class인 SliceImpl
public class SliceImpl<T> extends Chunk<T> {
private static final longserialVersionUID= 867755909294344406L;
private final boolean hasNext;
private final Pageable pageable;
public SliceImpl(List<T> content, Pageable pageable, boolean hasNext) {
super(content, pageable);
this.hasNext = hasNext;
this.pageable = pageable;
}
}
- 다음 페이지가 있는지 확인할 수 있는 hasNext, 그리고 Pageable 필드가 있다.
(3) List
- 추가 count 쿼리 없이 결과만 반환
정리
- Page를 통해서 사용 가능한 데이터의 총 개수 및 전체 페이지 수를 알 수 있다.
- 이는 카운트 쿼리를 실행함으로써 전체 페이지 수 및 전체 데이터의 개수를 알아낸다.
- 카운트 쿼리가 많은 비용이 드는 경우에는 Slice를 사용하면 된다. Slice는 다음 Slice가 존재하는지 여부만 알고 있다.
- 전체 데이터 셋의 크기가 큰 경우에는 Slice를 사용하는 게 성능상 유리하다.
- 결과를 단순히 List로 받을 수 있다. 단순히 주어진 범위 내의 엔티티를 검색하기 위한 쿼리만 실행된다.
3. count 쿼리 분리
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m) from Member m")
Page<Member> findByAge(int age, Pageable pageable);
- 데이터는 left join, 카운트는 left join 안 해도 된다.
- page는 count 쿼리까지 가져오므로 join이 많으면 카운트 쿼리 또한 join을 해서 가져온다 ⇒ 성능 저하
- count 쿼리는 join이 필요 없다.
- countQuery를 통해서 count 쿼리를 분리시켜서 성능을 향상시킨다.
4. Web에서의 페이징과 정렬
Spring Data Jpa에서는 HandlerMethodArgumentResolver 인터페이스 구현체를 제공한다.
- 페이징 기능 : PageableHandlerMethodArgumentResolver
- 정렬 기능 : SortHandlerMethodArgumentResolver
Example
@GetMapping("/posts")
public ApiResponse<Page<PostResponseDto>> getAllPosts (Pageable pageable) {
Page<PostResponseDto> posts = postService.getAllPosts(pageable);
return ApiResponse.ok(posts);
}
- 파라미터로 Pageable or Sort 을 받을 수 있다.
- PageableHandlerMethodArgumentResolver는 컨트롤러 메서드에 Pageable 타입의 파라미터가 존재하는 경우, 요청 파라미터를 토대로 PageRequest 객체를 생성한다.
(1) 요청 파라미터 (Request Parameter)
- ex) /member?page=0&size=3&sort=id,desc&sort=username,desc
- 쿼리 스트링으로 전달
- page : 현재 페이지, 0부터 시작한다.
- size : 한 페이지에 노출할 데이터 건수
- sort : 정렬 조건을 정의한다.
- ex) 정렬 속성, 정렬 속성 … (ASC | DESC), 정렬 방향을 변경하고 싶다면 sort 파라미터 추가 (asc 생략 가능)
(2) 기본값 설정
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = “username”,
direction = Sort.Direction.DESC) Pageable pageable) {
...
}
- Pageable의 기본값은 page=0, size=20 이다.
- @PageableDefault 어노테이션을 사용해서 기본값을 변경해줄 수 있다.
(3) 접두사를 통해 페이징 구분
- 페이징 정보가 둘 이상이면 접두사로 구분한다.
- @Qualifier에 접두사명 추가 “{접두사명}_xxx”
- ex) /members?member_page=0&order_page=1
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
반응형