Spring/Spring Data JPA

페이징과 정렬

민철킹 2021. 6. 13. 23:15

먼저 순수 JPA로 페이징과 정렬을 하는 법을 다시 한번 살펴보자.

 

검색 조건 : 나이가 10살

정렬 조건 : 이름으로 내림차순

페이징 조건 : 첫 번째 페이지, 페이지당 3개

 

totalpage = totalcount / size

JPQL을 작성하는데 offset 몇번째부터 limit 몇번째까지 데이터를 가져올 것인지 페이징을하고, 이름순으로 정렬하였다.

또한 전체 데이터의 수를 알기위해 totalcount메서드도 같이 작성

junit 테스트 작성

페이징 쿼리
totalcount 쿼리

원하는 대로 페이징이 성공한 것을 확인할 수 있다.

 

 


이번에는 스프링 데이터 JPA를 사용하여 페이징과 정렬을 해보자.

 

정렬 ==> org.springframework.data.domain.Sort

페이징 ==> org.springframework.data.domain.Pageable (내부에 'Sort' 포함)

 

스프링 데이터는 페이징과 정렬을 공통화해놓은 인터페이스를 제공한다.

반환 타입

org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit+1)
List(자바 컬렉션) : 추가 count 쿼리 없이 결과만 반환

 

이와 같이 반환 타입을 선택하고 parameter로 Pageable 받는 메서드 인터페이스만 생성해주면 된다.

이를 실제 사용하는 테스트를 진행해보겠다.

0부터 3개를 가지고 오고, username을 기준으로 내림차순 정렬을 진행하였다. (page는 0부터 시작)

이렇게 PageRequest에 페이징 조건을 넣어주면 된다. 반환타입을 Page로 하면 count 쿼리문까지 알아서 날려주기 때문에 페이징을 한 컨텐츠를 getContent로 가져오고 getTotalElements로 totalcount를 조회할 수 있고, getTotalPages를 통해 전체 요소 / 페이징 개수를 통해 몇개의 페이지가 생성되는지 또한 알려준다.

자동으로 count쿼리 수행

 

추가로 몇개의 유용한 메서드가 있는데

  • getNumber() ==> 페이징으로 가져온 페이지가 몇 페이지 인지?
  • isFirst() ==> 가져온게 첫번째 페이지야?
  • hasNext() ==> 다음 페이지가 존재해?
  • 이러한 메서드를 사용하여 다음 페이지로 넘기고 마지막 페이지임을 알려주는 것들이 가능하다!

 

이번에는 반환타입을 Page말고 Slice로 바꿔보자.

Slice는 전체를 조회하지 않기 때문에 getTotalPages, getTotalElements를 사용할 수 없다.

Slice는 내부적으로 limit+1을 조회하므로 우리가 설정한 크기 3보다 1큰 limit 4를 쿼리로 실행할 것이다.

최근 모바일 기반 웹에서 많이 사용하는 방식으로 밑에 페이지 번호를 클릭하여 움직이는 것이 아니라 더보기를 누르면 아래로 페이지가 더 추가되는 것을 볼 수 있을 것이다. 이를 활용하여 +1개가 더 존재한다면 미리 다음 페이지를 로딩하거나 클릭시 보여주고, +1개가 존재하지 않는다면 마지막 페이지라고 알려주는 식으로 설계를 하는 것이다.

 

반환 타입을 List를 사용해서 결과를 받을 수는 있지만 위와 같은 메서드들은 사용하지 못한다.

 

더보기

Page와 Slice 인터페이스

 

public interface Page extends Slice {

    int getTotalPages(); //전체 페이지 수

    long getTotalElements(); //전체 데이터 수

    Page map(Function converter); //변환기

}

 

public interface Slice extends Streamable {

    int getNumber(); //현재 페이지

    int getSize(); //페이지 크기

    int getNumberOfElements(); //현재 페이지에 나올 데이터 수

    List getContent(); //조회된 데이터

    boolean hasContent(); //조회된 데이터 존재 여부

    Sort getSort(); //정렬 정보

    boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부

    boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부

    boolean hasNext(); //다음 페이지 여부

    boolean hasPrevious(); //이전 페이지 여부

    Pageable getPageable(); //페이지 요청 정보

    Pageable nextPageable(); //다음 페이지 객체

    Pageable previousPageable();//이전 페이지 객체

    Slice map(Function converter); //변환기

}

 

totalCount는 어쨋든 DB에 있는 모든 데이터를 count 해야하므로 데이터가 많아질수록 성능에 문제가 발생한다.

특히 Member와 Team이 Left Outer Join을 하고 있다고 가정했을 때, 페이징을 하는 쿼리에는 당연히 join문이 포함되어야하지만, count를 하는 쿼리에는 join을 할 필요가 없다.(left join이기 때문에 기준이 Member이므로 row수는 변함없으므로, where문이 없다고 가정)

이 때에 Page를 사용하게되면 count를 하는 쿼리에도 join이 발생하여 성능 이슈가 발생할 수 있다.

쿼리문

 

이를 위해 countQuery를 따로 분리하는 기능이 존재한다.

쿼리문

 

 

참고로 top, first를 사용하여 페이징을 할 수 도 있다.(PageRequest를 넘기지 않고)

https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.limit-query-result

 

Spring Data JPA - Reference Documentation

Example 109. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del

docs.spring.io

 

 

당연히 실제 사용시에 위와 같이 Member 엔티티를 반환해서는 안된다. DTO로 변환해야한다.

페이지를 유지하면서 위와같이 DTO로 반환할 수 있기 때문에 위와 같은 방법을 사용하는 것이 좋다.

절대 엔티티를 직접 반환해서는 안된다!

반응형

'Spring > Spring Data JPA' 카테고리의 다른 글

@EntityGraph  (0) 2021.06.14
벌크성 수정 쿼리  (0) 2021.06.14
쿼리 메서드 기능  (0) 2021.06.11
Spring Data JPA 공통 인터페이스  (0) 2021.06.10
스프링 데이터 JPA 맛보기  (0) 2021.06.07