Spring Data JPA - 페이징과 정렬

목차

Spring Data JPA - 페이징과 정렬

조회 쿼리에 Pageable 객체를 넣어 줌으로써 JPA 에서 제공하는 Paging 기능을 사용할 수 있다.

스프링에서는 Page 을 손쉽게 사용할 수 있도록 Pageable 인터페이스를 제공합니다. 페이징시 offset (시작지점) 부터 시작해 limit 만큼씩 잘라 Page Number 를 붙입니다.

// Page 객체를 반환
Page<Member> findPageByAge(int age, Pageable pageable);
// Slice 객체를 반환
Slice<Member> findSliceByAge(int age, Pageable pageable);
// List 객체를 반환
List<Member> findListByAge(int age, Pageable pageable);

Paging 반환 값

Paging 반환되는 객체로 Page, Slice, List 세가지 종류의 객체가 있습니다.

종류 설명
Page Count 쿼리 를 포함하는 페이징
Slice 내부적으로 Limit + 1 조회
List 별도의 작업 없이 결과만 반환

Pageable 인터페이스

  • getPageNumber
    • 현재 페이지 번호를 가져옵니다.
  • getPageSize
    • 페이지당 데이터 개수를 가져옵니다.
  • getOffset
    • 페이징을 시작하는 시작 번호를 가져옵니다.
    • offset = page * size;
public interface Pageable {

static Pageable unpaged() {
return Unpaged.INSTANCE;
}

static Pageable ofSize(int pageSize) {
return PageRequest.of(0, pageSize);
}

default boolean isPaged() {
return true;
}

default boolean isUnpaged() {
return !isPaged();
}

int getPageNumber();

int getPageSize();

long getOffset();

Sort getSort();

default Sort getSortOr(Sort sort) {

Assert.notNull(sort, "Fallback Sort must not be null");

return getSort().isSorted() ? getSort() : sort;
}

Pageable next();

Pageable previousOrFirst();

Pageable first();

Pageable withPage(int pageNumber);

boolean hasPrevious();

default Optional<Pageable> toOptional() {
return isUnpaged() ? Optional.empty() : Optional.of(this);
}
}

PageRequest - Pageable 인터페이스 구현체

PageRequest 는 Pageable 구현체로 찾을 Page Index 정보, Paging 할 Size 를 이용해 생성할 수 있습니다. 만약 정렬햇 Paging 을 할 경우 Sort 객체를 추가로 넣어줄 수 있습니다.

PageRequest pageRequest = PageRequest
.of(0, 3);// Page Index가 0부터 시작한다.
// username 으로 정렬 후 Paging 합니다.
PageRequest pageRequest = PageRequest
.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));// Page Index가 0부터 시작한다.

테스트 코드 작성

Page 객체로 반환

// when
Page<Member> page = memberRepository.findPageByAge(10, pageRequest);

// then
List<Member> content = page.getContent();
long totalElement = page.getTotalElements();

for (Member member : content) {
System.out.println("member : " + member);
}
System.out.println("totalElements : " + totalElement);

assertThat(content.size()).isEqualTo(3); // paging 된 element 개수를 가져온다.
assertThat(page.getTotalElements()).isEqualTo(5); // 전체 element 개수를 가져온다.
assertThat(page.getNumber()).isEqualTo(0); // paging 시작 index를 가져온다.
assertThat(page.getTotalPages()).isEqualTo(2); // page 개수를 가져온다.
assertThat(page.isFirst()).isTrue(); // 첫번째 page인지 확인
assertThat(page.hasNext()).isTrue(); // 다음 page가 있는지 확인

Slice 객체로 반환

Slice<Member> slice = memberRepository.findSliceByAge(10, pageRequest);

assertThat(content.size()).isEqualTo(3); // paging 된 element 개수를 가져온다.
assertThat(slice.getNumber()).isEqualTo(0); // paging 시작 index를 가져온다.
assertThat(slice.isFirst()).isTrue(); // 첫번째 page인지 확인
assertThat(slice.hasNext()).isTrue(); // 다음 page가 있는지 확인

List 객체로 반환

List<Member> list = memberRepository.findListByAge(10, pageRequest);

전체 코드

@Test
public void paging(){
// given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 10));
memberRepository.save(new Member("member3", 10));
memberRepository.save(new Member("member4", 10));
memberRepository.save(new Member("member5", 10));

int age = 10;
PageRequest pageRequest = PageRequest
.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));// Page Index가 0부터 시작한다.

// when
Page<Member> page = memberRepository.findPageByAge(10, pageRequest);

// then
List<Member> content = page.getContent();
long totalElement = page.getTotalElements();

for (Member member : content) {
System.out.println("member : " + member);
}
System.out.println("totalElements : " + totalElement);

assertThat(content.size()).isEqualTo(3); // paging 된 element 개수를 가져온다.
assertThat(page.getTotalElements()).isEqualTo(5); // 전체 element 개수를 가져온다.
assertThat(page.getNumber()).isEqualTo(0); // paging 시작 index를 가져온다.
assertThat(page.getTotalPages()).isEqualTo(2); // page 개수를 가져온다.
assertThat(page.isFirst()).isTrue(); // 첫번째 page인지 확인
assertThat(page.hasNext()).isTrue(); // 다음 page가 있는지 확인

Slice<Member> slice = memberRepository.findSliceByAge(10, pageRequest);

assertThat(content.size()).isEqualTo(3); // paging 된 element 개수를 가져온다.
assertThat(slice.getNumber()).isEqualTo(0); // paging 시작 index를 가져온다.
assertThat(slice.isFirst()).isTrue(); // 첫번째 page인지 확인
assertThat(slice.hasNext()).isTrue(); // 다음 page가 있는지 확인

List<Member> list = memberRepository.findListByAge(10, pageRequest);

Page<Member> pageDivideCount = memberRepository.findPageDivideCountByAge(10, pageRequest);

List<Member> contentDivideCount = page.getContent();
long totalElementDivideCount = page.getTotalElements();

System.out.println("totalElementDivideCount : " + totalElementDivideCount);

assertThat(content.size()).isEqualTo(3); // paging 된 element 개수를 가져온다.
assertThat(pageDivideCount.getTotalElements()).isEqualTo(5); // 전체 element 개수를 가져온다.
assertThat(pageDivideCount.getNumber()).isEqualTo(0); // paging 시작 index를 가져온다.
assertThat(pageDivideCount.getTotalPages()).isEqualTo(2); // page 개수를 가져온다.
assertThat(pageDivideCount.isFirst()).isTrue(); // 첫번째 page인지 확인
assertThat(pageDivideCount.hasNext()).isTrue(); // 다음 page가 있는지 확인
}

Count 쿼리 분리하기

Page 객체를 반환하게 될 경우 Count 쿼리도 같이 실행이 된다. 이때 실행하고 싶은 Count 쿼리를 별도로 작성해 실행 시킬 수 있다.

@Query 사용시 countQuery 속성을 이용해 Count 쿼리를 분리할 수 있습니다.

Count 쿼리를 염두해 둬야 하는 이유는 객체끼리의 연관관계가 있을 경우 잘 못하면 N+1 의 문제가 발생할 수 있다.

// Count 쿼리 분리
@Query(value = "select m from Member m left join m.team t",
countQuery = "select count(m.username) from Member m")
Page<Member> findPageDivideCountByAge(int age, Pageable pageable);
Share