본문 바로가기

JPA

[Spring Data JPA] 페이징과 정렬

순수 JPA 페이징과 정렬

JPA에서 페이징을 어떻게 할 것인가?

 

페이징과 정렬을 사용하는 예제 코드

  • 검색 조건: 나이가 10살
  • 정렬 조건: 이름으로 내림차순
  • 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건

순수 JPA 페이징 리포지토리 코드

public List<Member> findByPage(int age, int offset, int limit) {
        return em.createQuery("select m from Member m where m.age = :age order by m.username desc ", Member.class)
                .setParameter("age", age)
                .setFirstResult(offset)
                .setMaxResults(limit)
                .getResultList();
    }

    public long totalCount(int age) {
        return em.createQuery("select count(m) from Member m where m.age = :age", Long.class)
                .setParameter("age", age)
                .getSingleResult();
    }

 

순수 JPA 페이징 테스트 코드

@Test
public void paging() {
    // given
    memberJpaRepository.save(new Member("member1", 10));
    memberJpaRepository.save(new Member("member2", 10));
    memberJpaRepository.save(new Member("member3", 10));
    memberJpaRepository.save(new Member("member4", 10));
    memberJpaRepository.save(new Member("member5", 10));
    
    int age = 10;
    int offset = 0;
    int limit = 3;

    // when
    List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
    long totalCount = memberJpaRepository.totalCount(age);

    /**
     * 페이지 계산 공식 적용...
     * totalPage = totalCount / size ...
     * 마지막 페이지 ...
     * 최초 페이지 ..
     */

    // then
    assertThat(members.size()).isEqualTo(3);
    assertThat(totalCount).isEqualTo(5);
}

스프링 데이터 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 쿼리 없이 결과만 반환

페이징과 정렬 사용 예제

Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함 
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함 
List<Member> findByUsername(String name, Sort sort);

Page 사용 예제 정의 코드

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

Page 사용 예제 실행 코드

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

    // when
    Page<Member> page = memberRepository.findByAge(age, pageRequest);

    // then
    List<Member> content = page.getContent(); //조회된 데이터
    assertThat(content.size()).isEqualTo(3); //조회된 데이터 수 
    assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
    assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
    assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
    assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
    assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
}
  • 두 번째 파라미터로 받은 Pageable 은 인터페이스다.
  • 따라서 실제 사용할 때는 해당 인터페이스를 구현한 'org.springframework.data.domain.PageRequest' 객체를 사용한다.
  • PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다.
  • 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다.
  • 참고로 페이지는 0부터 시작한다.

Page 인터페이스

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

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

 

참고: 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);
  • 카운트 쿼리 분리(이건 복잡한 sql에서 사용, 데이터는 left join, 카운트는 left join 안해도 됨)

※ 스프링 부트 3 - 하이버네이트 6 left join 최적화 설명 추가

스프링 부트 3 이상을 사용하면 하이버네이트 6이 적용된다. 이 경우 하이버네이트 6에서 의미없는 left join을 최적화 해버린다.

따라서 다음을 실행하면 SQLLEFT JOIN을 하지 않는 것으로 보인다.

@Query(value = "select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable);
  • 실행한 JPQL을 보면 left join을 사용하고 있다.
  • Member와 Team을 조인을 하지만 사실 이 쿼리를 `Team` 을 전혀 사용하지 않는다.
  • select 절이나, where절에서 사용하지 않는 다는 뜻이다.
  • 그렇다면 이 JPQL은 사실상 다음과 같다. `select m from Member m`
  • `left join` 이기 때문에 왼쪽에 있는 `member` 자체를 다 조회한다는 뜻이 된다.
  • 만약 `select` 나, `where` 에 `team` 의 조건이 들어간다면 정상적인 `join` 문이 보인다.
  • JPA는 이 경우 최적화를 해서 `join` 없이 해당 내용만으로 SQL을 만든다.

여기서 만약 `Member` `Team` 을 하나의 SQL로 한번에 조회하고 싶다면 JPA가 제공하는 `fetch join` 을 사용 해야한다.

+ select m from Member m left join fetch m.team t` 이 경우에도 SQL에서 join문은 정상 수행된다.

페이지를 유지하면서 엔티티를 DTO로 변환하기

 Page<Member> page = memberRepository.findByAge(10, pageRequest);
 Page<MemberDto> dtoPage = page.map(m -> new MemberDto());
  • API를 개발할 때 Page 자체를 ResponseBody로 내보내도 Page에 있는 totalCount 라든지, TotalElements 같은 정보가 들어있기 때문에 되게 유용하다.
  • Page<Member> 에서 엔티티를 그대로 내보내면 API 스펙이 바뀌는 문제가 발생하기 때문에 절대로 DTO로 변환 후 반환
    • (만약 Member 클래스의 필드나 데이터 타입이 변경되면, 클라이언트로 반환되는 JSON 응답의 구조도 변경되는 문제)
    • 결론적으로, "API 스펙을 바꾼다"는 말은 클라이언트와 서버 간 데이터 교환의 계약을 깨뜨리는 것을 의미
 

'JPA' 카테고리의 다른 글

[Spring Data JPA] @EntityGraph  (0) 2025.01.17
[Spring Data JPA] 벌크성 수정 쿼리  (0) 2025.01.17
[JPA] OSIV와 성능 최적화  (0) 2025.01.10
[JPA] 컬렉션 조회 최적화  (0) 2025.01.10
[JPA] 지연 로딩과 조회 성능 최적화  (0) 2025.01.07