Spring/자바 ORM 표준 JPA 프로그래밍

[Spring Data JPA] 2-2. JPA 페이징과 정렬

kyung.Kh 2024. 8. 30. 02:48

JPA 페이징과 정렬

Spring Data JPA는 페이징 및 정렬 기능을 지원한다.

순수 JPA 페이징과 정렬

아래의 조건으로 페이징과 정렬을 사용해 보겠다

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

위와 같이 페이징을 할 때 필요한 offset과 limit로 잘라서 가져오는 쿼리와 totalCount를 가져오는 두 가지의 쿼리를 작성했다.

이를 테스트해보면 아래와 같다

@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 = 1;
    int limit = 3;

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

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

Spring Data JPA 페이징과 정렬

Spring Data JPA에서는 페이징 및 정렬 기능을 제공한다.

페이징과 정렬 관련 파라미터

  • org.springframework.data.domain.Sort : 정렬을 위한 클래스
  • org.springframework.data.domain.Pageable : 페이징 기능을 제공. 내부에 Sort를 포함하고 있음.

반환 타입에 따른 특징

  • org.springframework.data.domain.Page : 추가 count 쿼리의 결과를 포함하는 페이징 (ex: 페이지 내릴 때, "더보기"를 누르면 다음 결과들이 이어서 출력됨)
  • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능(내부적으로 limit + 1 조회)
  • List : 추가 count 쿼리 없이 결과만 반환

페이징과 정렬 사용 예제

페이징을 사용하여 나이가 10살인 member 중, 이름으로 내림차순으로 정렬하겠다. 단, 페이지 조건은 첫 번째 페이지, 페이지당 보여줄 데이터는 3건이다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    Page<Member> findByAge(int age, Pageable pageable);  
}
@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"));// 0페이지에서 3개 가져와라. sort의 방향은 DESC(정렬 조건은 선택)

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

    // then
    List<Member> content = page.getContent();  // page 내부에 있는 3개(pageSize)를 꺼내옴

    assertThat(content.size()).isEqualTo(3);  // 페이지 사이즈
    assertThat(page.getTotalElements()).isEqualTo(5);
    assertThat(page.getNumber()).isEqualTo(0);  // 페이지 번호
    assertThat(page.getTotalPages()).isEqualTo(2);// 총 페이지 개수(알아서 계산됨) - 페이지 사이즈는 3인데 member가 5명이니깐 2가 나와야됨
    assertThat(page.isFirst()).isTrue();  // 첫 페이지인가??
    assertThat(page.hasNext()).isTrue();  // 다음 페이지가 있나?? (게시판의 다음페이지, 이전페이지 같은데서 사용)
}

Pageable은 인터페이스이므로, org.springframework.data.domain.PageRequest 객체를 사용한다. PageRequest 생성자의 첫 번째 파라미터는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터의 수를 입력한다. 페이지는 0부터 시작한다.

 

다음으로는 Slice를 사용해보겠다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    Slice<Member> findByAge(int age, Pageable pageable);  // 페이징 슬라이스
}
@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"));// 0페이지에서 3개 가져와라. sort의 방향은 DESC(정렬 조건은 선택)

    // when
    Slice<Member> page = memberRepository.findByAge(age, pageRequest);  // Slice로 바꿔야 됨

    // then
    List<Member> content = page.getContent();  // page 내부에 있는 3개(pageSize)를 꺼내옴

    assertThat(content.size()).isEqualTo(3);  // 페이지 사이즈
    assertThat(page.getNumber()).isEqualTo(0);  // 페이지 번호
    assertThat(page.isFirst()).isTrue();  // 첫 페이지인가??
    assertThat(page.hasNext()).isTrue();  // 다음 페이지가 있나?? (게시판의 다음페이지, 이전페이지 같은데서 사용)
}

결과 쿼리를 보면 limit가 4개인 것을 확인할 수 있다. 이는 우리가 요청한 것보다 1개 더 요청한 것이다. 이는 모바일 디바이스에서 많이 사용하며 리스트를 내리다 보면 만약에 조회할 내용이 하나 더 있으면 더보기 버튼이라든지 로딩을 한다는지 할 때 사용하는 방법이다.

타입만 Slice로 바꾸면 간단하게 사용이 가능하다.

 

다음으로는 List를 적용해보겠다.

public interface MemberRepository extends JpaRepository<Member, Long> {
    List<Member> findByAge(int age, Pageable pageable);
}
@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"));// 0페이지에서 3개 가져와라. sort의 방향은 DESC(정렬 조건은 선택)

    // when
    List<Member> page = memberRepository.findByAge(age, pageRequest);  // List로 수정
}

리스트를 사용하는 경우는 단순히 리스트를 개수 단위로 끊어서 가져오고 싶을 때 사용한다.

복잡한 쿼리의 경우 countQuery를 사용하여 분리

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query(value = "select m from Member m left join m.team t", 
           countQuery = "select count(m.username) from Member m")
    Page<Member> findByAge(int age, Pageable pageable); 
}

User findTopByOrderByAgeDesc()나 User findFirstByOrderByLastnameAsc(); 처럼 Top, First와 같은 키워드를 사용하여 결과의 제한된 수를 조회할 수 있다.

더 자세한 정보는 https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#repositories.limit-query-result 에서 참고할 수 있다.

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

// when
Page<Member> page = memberRepository.findByAge(age, pageRequest);
Page<MemberDto> toMap = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));  // Page를 유지하면서 Dto로 변환 가능

 


[출처]

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84 → 이 글은 김영한님의 "실전! 스프링 데이터 JPA" 강의 중 5장을 듣고 정리한 내용입니다.

 

 

728x90