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, ...

 

반응형