Web Dev/Spring

Spring - Paging 처리

DuL2 2022. 8. 22. 01:22

Pagination

본인은 전통적인 서블릿 JSP 기반 프로젝트에서 페이지네이션을 하기 위에 List를 받아와 나누어 사용하는 Paging 알고리즘을 이용하여 해본 경험 뿐이 없었다. Pagination을 깔끔하고 이쁘게 만들려면 상당히 복잡했던 기억이 있다. 이 후 Spring을 유튜브를 통해 처음 공부하면서 스프링에서 표준화한 Pageable 객체를 처음 접했고, 이런 방법이 있구나 하면서 신기해 했던 기억이 난다.

Spring Data JPA를 통해서 Paging을 할 것이므로 먼저 문서를 찾아보기로 했다.


1. Spring Data JPA 공식 문서 Page 검색해보기

검색해보니 현재 기준 대략 105건 정도가 나온다.

1. 3.4.4. Special parameter handling

  • 첫번째로 다수의 검색결과가 나오는 지점이다. 파라미터로 Pageable과 Sort 를 받아서 pagination과 sorting을 통해 쿼리를 동적으로 할 수 있다는 것 같다.
  • 메소드에 Pageable 객체를 넣음으로서 Page, Slice, List를 반환 받을 수 있다.(너무나 편한 것..)

예시

아래 문서를 간단히 읽어보면 첫번째 메소드에 대한 설명이 있다. 부족한 영어로 이해해보자면..

Pageable 인스턴스를 통해서 정적으로 정의된 쿼리를 동적으로 페이징 할 수 있다고 합니다. `Page` 객체는 총 elements(테이블의 행, 값)의 총 갯수를 알고있고, 몇 page가 나올 수 있는지 알고 있답니다. 그렇다면 수십만 건이 있다면 계산 비용이 많이 들겠죠?

이와 달리 Slice 는 단순히 다음에 사용할 Slise 가 있는지만 알고 있다고 합니다.

Sorting 도 Pageable 인스턴스에 의해 조작할 수 있다고 합니다. 또 Sorting 만 필요한 거라면 `org.springframework.data.domain.Sort` 파라미터를 넣어 Sorting을 할 수 있으며, 아니면 간단하게 List로 받아볼 수도 있다고 합니다.

 

2. 3.4.5. Limiting query results

  • `first`나 `top` 키워드로 제한하여 가져올 수 도 있습니다. (숫자를 생략할 경우 이므로 1개만 가져옴)
  • 키워드 뒤에 특정 숫자를 붙여 최대 결과 갯수를 제한하여 받아 올 수도 있습니다.

Limiting 표현은 `Distinct` 키워드를 지원합니다. 또한, 하나의 result set으로 제한하는 경우에 Optional로 래핑하여 받을 수 있도록 지원합니다.

만약 페이지네이션이랑 slicing이 제한된 쿼리 페이지네이션에서 사용되면 제한된 결과 내에서 적용됩니다.

 

3. HandlerMethodArgumentResolvers for Pageable and Sort

다음으로 page에 설명되어 있는 부분은 3.8.2. Web support 파트에서 입니다.

읽어보면 Spring MVC 가 Pageable 인스턴스를 request parameters로 받아 사용할 수 있도록 해준다고 합니다.

따라서 Pageable 객체를 컨트롤러 메소드의 파라미터로 넣는다면 다음 세 값을 request parameter에 넣을 수 있습니다.

   
page 되돌려 받고 싶은 Page 값, 0으로 인덱싱되고, 기본값은 0
size 되돌려 받고 싶은 페이지의 사이즈, 기본값은 20
sort property,property(ASC|DESC) / ?sort=firstname&sort=lastname,asc

 

  • 커스터 마이징 가능.
    • PageableHandlerMethodArgumentResolverCustomizer나 SortHandlerMethodArgumentResolverCustomizer 인터페이스를 추상화한 bean을 등록해서 커스터 마이징 가능함.
  • 다중 Pegeable or Sort 인스턴스가 필요하면 `@Qualifier`로 구별하여 받아 사용하면 됨.
    • `@Qualifier("foo") Pageable first, @Qualifier("bar") Pageable second` 이런 식으로
  • Pageable의 기본 값은 new PageRequest(0, 20)과 동일함.
    • 하지만 `@PageableDefault` 어노테이션으로 Pageable 파라미터를 설정해줄 수 있음.
  • Hypermedia support for Pageables
    • ResourceAssembler 인터페이스가 추상화된 PagedResourcesAssembler에 의해서 Page 객체가 PagedResources로 변환됨.

 

간단하게 문서를 읽어봤지만 HATEOAS 부분은 사용해보지 않아서 잘 모르겠습니다.

 

 


Spring Data JPA의 페이징 처리

Spring Data JPA 에서 페이징과 정렬 파라미터를 공통화, 표준화 시켜서 패키지에 포함시켰습니다.

  • org.springframework.data.domain.Sort: 정렬기능
  • org.springframework.data.domain.Pageable: 페이징 기능(내부에 정렬(Sort) 기능 포함)

 

또한 Repo에서 Page같은 특별한 타입을 리턴할 수 있도록 만들어놨습니다.

  • org.springframework.data.domain.Page: 추가 count 쿼리 결과를 포함하는 페이징

pagination 예시

  • org.springframework.data.domain.Slice: 추가 count 쿼리 없이 페이지만 확인 가능( 내부적으로 limit + 1 조회)
    • totalCount가 필요하지 않은 버튼을 만들때 사용할 수 있다.
    • 예를들어 더보기 같은 기능(더보기는 토탈 페이지가 필요 없음.)

더보기 기능 예시

 

Page 소개

메소드 쿼리 Pageable 사용

페이징 처리를 하고 싶다면 기존 메소드 쿼리에 Pageable객체를 넣어주면 됩니다.

Pageable 인터페이스는 쿼리에 대한 조건을 담고 있고, 그에 맞는 Page를 반환해줍니다.

    Page<Member> findByAge(int age, Pageable pageable);

다만 실제로 넣을 때는 보통 Pageable의 구현체인 PageRequest를 사용합니다.

    PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

PageRequest 파라미터는 각각 몇번째 페이지(page), 그 페이지에서 몇 개 가져올지(size), 정렬기준(Sort, 정렬할 속성(properties))를 의미합니다.

 

Page 특징

Page 로 받게 되면 내부에 컨텐츠와 Pagination에 사용하기 위한 총 페이지 갯수와 같은 값들이 자동으로 계산되어 들어있습니다. Repository에서 컨텐츠를 limit 3과 offset을 설정해서 가져옴과 동시에 count 쿼리를 날려 총 페이지 갯수, 총 element 수 등을 받아옵니다. 또한, 현재 페이지 번호(page.getNumber())도 받아 올 수 있습니다.

본 쿼리 예시
카운트 쿼리 예시

 

사용할 수 있는 Page 메소드

Slice > Page: 상속관계에 있어서 Slice가 더 부모라 Page가 오토캐스팅됩니다. Page는 Slice의 모든 메소드를 가지고 있습니다.

Page 메소드 사용할 수 있는 메소드
page.getTotalElements() 총 element 갯수
page.getTotalPages() 총 페이지 갯수

 

Slice 소개

Slice는 요청 쿼리를 날릴 때 Limit + 1 갯수의 값을 요청하게 된다. 예를들어 위에서 0번째 페이지에서 3개의 element를 받아오도록 했는데 Slice는 3 + 1하여 총 4개를 요청합니다.

Slice는 Page와 달리 TotalPage 수를 가져오지 않으며, 총 element 갯수에 대한 쿼리도 날리지 않습니다. 단지 다음 값이 존재하는 지만 확인하기 위해 Limit + 1 갯수의 값을 요청하는 것입니다.

JPA에서 표준화를 한 덕분에 개발 도중 Page가 TotalCount 때문에 무거워 부담스럽다면 Slice로 쉽게 변경할 수 있다는 강점이 있습니다.

주의: Page는 1부터 시작이 아니라 0부터 시작입니다.

 

사용할 수 있는 slice 메소드

slice 메소드 사용할 수 있는 메소드
List slice.getContent() 조회된 데이터
int slice.getNumber() 현재 페이지 숫자
int slice.getSize() 페이지 크기
int slice.getNumberOfElements() 현재 페이지에 나올 데이터 수
boolean slice.hasContent() 조회된 데이터 존재 여부
Sort slice.getSort() 정렬 정보
boolean slice.isFirst() 첫 페이지 인지
boolean slice.isLast() 마지막 페이지 인지
boolean slice.hasNext() 다음 페이지가 있는지
boolean slice.hasPrevious() 이전 페이지가 있는지
Pegeable slice.getPageable() 페이지 요청 정보
Pegeable slice.nextPageable() 다음 페이지 객체
Pegeable slice.previousPageable() 이전 페이지 객체
slice.map() 변환기

 

Page와 Slice를 List로 받기

Pageable 파라미터로 쿼리를 해오면 반환 타입에 따라서 알맞게 받아올 수 있습니다.

List로 쉽게 받아 사용 가능합니다.

    List<Member> list = memberRepository.findByAge(age, pageRequest);

 

Count Query 분리하기

Page가 아무래도 수십만 건의 DB 데이터의 갯수를 조회하고 세서 totalCount를 받아오다보니 아무리 최적화가 잘 되어있어도 무거울 수 있습니다. 이럴 때를 대비하여 Spring Data JPA가 Count Query를 분리하여 사용할 수 있도록 해놓았습니다.

@Query(value = "select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable);

위 메소드를 통해서 Page를 받아올 때는 당연히 본 쿼리와 Page 객체로 받아오기 때문에 총 element의 갯수를 세야합니다. 이 때 성능이 나빠질 수 있는데 단순히 element의 갯수만 세서 오면 되는 count Query도 위 사진의 쿼리처럼 left join해서 가져오기 때문입니다. 그렇기 때문에 다음처럼 countQuery 옵션을 사용하여 member의 갯수만 세서 오면 성능이 좋아집니다. 굳이 할 필요없는 left join을 안하기 때문입니다.

@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);

 

Limit 쿼리

First, Top을 붙여서 메소드를 만들 수도 있습니다. JPA 공식 문서에서 봤던 부분.

 

쉽게 Entity를 DTO로 변환하기

Page Interface 내부에 map 메소드가 있어 쉽게 변환 가능합니다.

Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId, member.getUsername(), null));

 

Web 확장 - 페이징과 정렬

공식문서 3.8.2 Web Support 부분을 더 디테일하게 설명해 놓은 것이다.

Spring은 parameter로 들어오는 Pegeable의 필드 값들을 직접 받을 수 있도록 해줍니다.

  • page
  • size
  • Sort

 

Pegeable default 값 글로벌 설정

application.yml 파일에서 설정을 해줄 수 있습니다.

spring:
  data:
    web:
      pageable:
        default-page-size: 10
        max-page-size: 2000

 

Pegeable default 값 Controller 로컬 설정

간단하다 Controller에서 받는 파라미터에 @PageableDefault(size = , dort = "속성") 어노테이션으로 설정해줄 수 있습니다.

 

접두사 - 두 가지 이상의 Paging parameters 처리

@Qualifier 어노테이션으로 구분해주면 됩니다.

String showUsers(Model model,
      @Qualifier("memeber") Pageable first,
      @Qualifier("order") Pageable second) { … }

 

Page를 1부터 시작하고 싶다면?

  1. Pageable, Page를 직접 커스터마이징 해서 처리합니다. 그리고 직접 PageRequest(Pageable 구현체)를 생성해서 레포지토리에 넘기면 됩니다. 물론 응답 값도 Page 대신에 직접 만들어서 제공해야 합니다.
  2. spring.data.web.pageable.one-indexed-parameterstrue로 설정합니다. 이 방법은 web에서 page 파라미터를 -1 처리할 뿐입니다. 따라서 응답값인 Page에 모두 0 페이지 인덱스를 사용하는 한계가 있습니다.
spring:
  data:
    web:
      pageable:
        one-indexed-parameters: true
  • 한계점
    • page 내부의 정보값이 맞지 않는다.
    • 2 page를 조회해왔지만 page 값은 1을 가리키기도 한다.