오봉이와 함께하는 개발 블로그

스프링 데이터 JPA - 스프링 데이터 JPA 페이징과 정렬 본문

BE/JPA

스프링 데이터 JPA - 스프링 데이터 JPA 페이징과 정렬

오봉봉이 2022. 9. 13. 02:11
728x90

스프링 데이터 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는 보통 우리가 아는 1 ~ n으로 이뤄진 페이지를 구현할 때 사용하고, Slice는 모바일 환경에서 스크롤에 따라 자동으로 데이터가 노출되거나, 버튼(예 : 더보기 버튼)을 통해 페이징을 할 때 주로 사용한다.
페이징을 사용하지 않고, 데이터만 원하는 만큼 반환받고 싶을 때는 일반적인 List로 반환 받으면 된다.

Pageable, Page등 인터페이스가 개발된 이후 개발자는 JPQL을 직접 짜더라도 페이징이나 totalCount같은 외적인 관심사를 제외하고 집중해야 하는 쿼리만 짜면 된다.

페이징과 정렬 사용 예제

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);
  • 조건
    • 검색 조건 : 나이가 10살
    • 정렬 조건 : 이름으로 내림차순
    • 페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 3건

Page 인터페이스

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

Page 사용 예제 코드

public interface MemberRepository extends JpaRepository<Member, Long> {
    Page<Member> findPageByAge(int age, Pageable pageable);
}
@Test
public void paging() throws Exception {
    // 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))

    // page 1 = offset = 0, limit = 10
    // page 2 = offset = 10, limit = 10
    // ..........

    int age = 10;
    // PageRequest는 Pageable의 구현체다.
    // PageRequest.of(page, size, sorting 조건);
    PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"))

    // when
    Page<Member> page = memberRepository.findPageByAge(age, pageRequest);
    // 반한 타입이 Page라서 totalCount가 필요한 것을 알기 때문에 totalCount 쿼리를 같이 날려서 totalCount 쿼리는 필요 없다

    // 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(); //다음 페이지가 있는가?
}

PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다.
여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다.
참고로 페이지는 0부터 시작한다.
두 번째 파라미터로 받은 Pagable은 인터페이스다.
따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest객체를 사용한다.
Page는 1부터 시작이 아니라 0부터 시작이기 때문에 주의하자.

Slice 인터페이스

SlicetotalCount를 가져오지 않기 때문에 다음 페이지가 있는지, 없는지에 대한 여부로 페이징을 한다.

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 사용 예제 코드

public interface MemberRepository extends JpaRepository<Member, Long> {
    Slice<Member> findSliceByAge(int age, Pageable pageable);
}
    @Test
    public void paging() throws Exception {
        // 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));

        // page 1 = offset = 0, limit = 10
        // page 2 = offset = 10, limit = 10
        // ...........

        int age = 10;
        // PageRequest는 Pageable의 구현체다.
        // PageRequest.of(page, size, sorting 조건);
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

        // when
        Slice<Member> slice = memberRepository.findSliceByAge(age, pageRequest);

        // then
        List<Member> contentSlice = slice.getContent(); //조회된 데이터
        assertThat(contentSlice.size()).isEqualTo(3); //조회된 데이터 수
        // slice에는 구현되지 않음 assertThat(slice.getTotalElements()).isEqualTo(5); //전체 데이터 수
        assertThat(slice.getNumber()).isEqualTo(0); //페이지 번호
        // slice에는 구현되지 않음 assertThat(slice.getTotalPages()).isEqualTo(2); //전체 페이지 번호
        assertThat(slice.isFirst()).isTrue(); //첫번째 항목인가?
        assertThat(slice.hasNext()).isTrue(); //다음 페이지가 있는가?
    }

List 사용 예제 코드

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findListByAge(int age, Pageable pageable);
}
@Test
    public void paging() throws Exception {
        // 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));

        // page 1 = offset = 0, limit = 10
        // page 2 = offset = 10, limit = 10
        // ...........

        int age = 10;
        // PageRequest는 Pageable의 구현체다.
        // PageRequest.of(page, size, sorting 조건);
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

        // when
        List<Member> list = memberRepository.findListByAge(age, pageRequest);

        // then
    }
select
        member0_.member_id as member_i1_0_,
        member0_.age as age2_0_,
        member0_.team_id as team_id4_0_,
        member0_.username as username3_0_
from
    member member0_
where
    member0_.age=?
order by
    member0_.username desc limit ?

select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.team_id
as team_id4_0_, member0_.username as username3_0_
from member member0_
where member0_.age=10
order by member0_.username desc limit 3;

실무에서 중요한 점 1

페이징 쿼리를 잘 사용하지 않는 이유는 totalCount쿼리가 DB에 있는 모든 데이터를 카운트 해야하기 때문에 성능이 좋지 않아서다.
where문을 사용하지 않는 가정하에 left join을 하게 될 때, 데이터 갯수의 결과는 join을 하나, 하지 않으나 똑같기 때문에 left join에 대한 totalCount는 필요가 없어서 쿼리를 잘 짜야한다.

다음과 같이 count 쿼리를 분리할 수 있다.

@Query(value = “select m from Member m left join m.team”,
        countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);

실무에서 중요한 점 2

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

@Query(value = "select m from Member m order by m.username desc")

정렬 조건이 너무 복잡하면 Pageable(PageResult)에서 잘 작동하지 않기 때문에 @Query어노테이션을 이용해서 JPQL로 정렬 조건을 작성하자.

실무에서 중요한 점 2

Page<Member> page = memberRepository.findPageByAge(age, pageRequest);

엔티티는 외부에 노출시키면 안 되기 때문에 위 코드로 API 응답을 하면 안 된다.(위 코드는 엔티티임)

페이지를 유지하면서 엔티티를 DTO로 변환해서 응답하자.

Page<MemberDto> dtoPage = page.map(m -> new MemberDto(m.getId(), m.getUsername(), m.getTeam().getName()));

위 코드로 응답하면 MemberDto로 응답하기 때문에 반환해도 되고, 내부 메소드를 통해 JSON으로 데이터가 잘 반환된다.

인프런 김영한 지식공유자님 강의 : 실전! 스프링 데이터 JPA
728x90
Comments