[JPA] 페이징과 정렬에 대해 알아보자.

페이징 (Pageable, Page, Slice )

데이터베이스의 대용량 데이터를 처리할 때, 데이터를 효율적으로 로드하고 출력하기 위해 페이지 단위로 데이터를 분할하여 가져오는 기능을 페이징이라고 한다.

 

페이징 적용 코드

다음과 같은 순서와 방식으로 페이징을 적용할 수 있다.

public interface MemberRepository extends JpaRepository<Member, Long> {
	Page<Member> findByAge(int age, Pageable pageable);
}

 

/** 
* 1. PageRequest 객체 생성
* PageRequest.of() 메소드를 사용하여 페이지 번호와 페이지 크기를 전달하며 PageRequest 객체를 생성할 수 있다. 
* 이때 정렬 조건이 있는 경우 추가적으로 Sort 객체도 함께 인자로 전달 가능하다.
*/
Pageable pageRequest = PageRequest.of(0, 10); // 페이지 번호: 0, 페이지당 크기: 10

/** 
* 2. Repository에 Pageable 전달
* Pageable 객체를 Repository 메소드의 매개변수로 전달하여 페이징 처리를 적용
*/
Page<Member> members = memberRepository.findByAge(10, pageRequest);

/**
* 3. 결과 사용
*  Repository에서 반환된 Page 객체를 사용하여 결과 정보와 페이징 메타데이터 등을 다룰 수 있다.
* (엔티티를 직접 조회하지 않고 다음과 같이 DTO로 변환해 사용해야 안전하다.)
*/
Page<MemberDto> memberDtos = page.map(m -> new MemberDto(m.getId(), m.getUsername()));

int totalPages = memberDtos.getTotalPages();  // 총 페이지 수 얻기
int pageNumber = memberDtos.getNumber();      // 현재 페이지 번호 얻기
List<MemberDto> memberList = memberDtos.getContent(); // 조회된 데이터 얻기

 

이제 페이징에 사용되는 Pageable, Page에 대해 알아보자.

 

Pageable

Pageable은 Spring Data에서 페이징 및 정렬을 처리하기 위한 인터페이스이다. (내부에 Sort 포함)

데이터를 한 페이지 단위로 가져오고 정렬하는 작업에 필요한 정보(페이지 번호, 페이지 크기, 정렬 방법)를 담고 있다.

Pageable 객체를 사용하여 Repository에 페이징과 정렬 조건을 전달하고, 결과로 Page 또는 Slice 객체를 반환받아 사용한다.

(참고로 Spring Data JPA는 페이지가 0부터 시작한다.)

 

Slice 와 Page

Slice

다음 페이지만 확인 가능하다. ( ex: 모바일에 더보기 기능, 무한 스크롤 )

따라서, 전체 페이지 개수를 모르기 때문에 추가 totalCount 쿼리가 실행되지 않는다. ( 성능 낭비 발생하지 않음 )

또한, Slice는 내부적으로 limit + 1 수행한다.

limit + 1이란 페이지당 보여줄 데이터 수를 3으로 지정했다면  3 + 1이 되어 4개를 가져온다.

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); //변환기
}

 (생성된 쿼리 확인해보면 limit 3 이 아닌, limit 4 즉, 내부적으로 limit + 1해서 쿼리를 생성)

 

Page

Page 인터페이스는 Slice 인터페이스를 상속한다.

따라서 Slice의 기능 외에 추가적인 기능(전체 페이지, 전체 데이터 수)들을 갖고 있다.

public interface Page<T> extends Slice<T> {
    int getTotalPages(); //전체 페이지 수
    long getTotalElements(); //전체 데이터 수
    <U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}

추가 totalCount 쿼리 결과 포함되어, 전체 페이지 수와 전체 데이터 개수를 알 수 있다.

 

Slice와 Page 차이

Slice와 Page는 전체 페이지 수와 전체 데이터 개수를 조회하는지 안하는지에 대한 차이를 갖고 있다.

따라서 Page는 전체 페이지와 전체 데이터 수를 계산하기 위해 DB 내에서 주어진 조건에 따른 Count 쿼리를 추가로 호출한다.

반면 Slice는 다음 페이지가 있느지만 확인하기 때문에 Page보다 성능이 더 좋다.

 

무엇을 사용할지는 다음과 같이 정리할 수 있다.

  • Page : 게시판과 같이 총 데이터 갯수가 필요한 환경
  • Slice :  모바일과 같이 총 데이터 갯수가 필요없는 환경에서(무한스크롤 등)

 

Count 쿼리 분리 - 성능 최적화

데이터를 가져오는 쿼리가 복잡하면, count 쿼리도 복잡해져서 성능 최적화가 필요할 때 사용하는 방법으로

count 쿼리를 다음과 같이 분리할 수 있다. (totalCount 쿼리는 매우 무겁다.)

// totalCount 성능 최적화를 위한, 쿼리 분리(값 가져오는 쿼리, totalCount 쿼리)
@Query(value = "select m from Member m", countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);

다대일 left outer join을 할 때, 데이터를 가져올 때는 join을 해야하지만

totalCount 쿼리에서는 join을 하지 않아도 count 값은 같다.

 


정렬 ( Sort )

데이터베이스로부터 가져온 데이터를 특정 필드에 기준으로 순서대로 정렬하는 기능으로,

Spring Data JPA에서는 Sort 클래스를 사용하여 정렬을 수행할 수 있다.

Sort 인스턴스를 생성하고 원하는 정렬 조건과 방향(오름차순 또는 내림차순)을 설정한 뒤, 레포지토리에 인수로 전달하여 정렬을 적용할 수 있다.

Sort sort = Sort.by(Sort.Direction.DESC, "createdDate"); // 생성일 기준 내림차순 정렬
List<User> users = userRepository.findAll(sort);

페이징 + 정렬

페이징과 정렬은 다음과 같이 함께 사용할 수 있다.

PageRequest.of() 메서드를 사용페이지 번호와 페이지 크기를 지정하면서 Sort 인스턴스를 전달하여 페이징과 정렬을 동시에 적용할 수 있다.

public interface MemberRepository extends JpaRepository<Member, Long> {
	Page<Member> findByAge(int age, Pageable pageable);
}
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); // username 기준 내림 차순
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> memberDtos = page.map(m -> new MemberDto(m.getId(), m.getUsername()));