본문 바로가기

JPA 실전

[JPA] JPA 실전 - 쿼리 메소드 (2) -페이징과 정렬

 

 

 

순수 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")
             .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)
}

 

 

JPA 페이징 테스트 코드

@Test
 public void paging() throws Exception {
//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 쿼리 없이 결과만 반환

주요 클래스 및 인터페이스

  • Sort: 정렬 기능을 제공합니다.
  • Pageable: 페이징 기능을 제공하며, 내부에 Sort를 포함합니다.
  • Page: 추가 count 쿼리 결과를 포함하는 페이징 기능을 제공합니다.
  • Slice: 추가 count 쿼리 없이 다음 페이지만 확인 가능한 페이징 기능을 제공합니다.
  • 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 반환 타입 (count 쿼리 사용)

Page<Member> findByUsername(String name, Pageable pageable);
Pageable pageable = PageRequest.of(0, 10, Sort.by("username").ascending());
Page<Member> page = memberRepository.findByUsername("John", pageable);

System.out.println("Total Pages: " + page.getTotalPages());
System.out.println("Total Elements: " + page.getTotalElements());
page.getContent().forEach(System.out::println);

 

Slice 반환 타입 (count 쿼리 사용 안함)

Slice<Member> findByUsername(String name, Pageable pageable);
Pageable pageable = PageRequest.of(0, 10, Sort.by("username").ascending());
Slice<Member> slice = memberRepository.findByUsername("John", pageable);

System.out.println("Has next: " + slice.hasNext());
slice.getContent().forEach(System.out::println);

 

List 반환 타입 (count 쿼리 사용 안함)

List<Member> findByUsername(String name, Pageable pageable);
Pageable pageable = PageRequest.of(0, 10, Sort.by("username").ascending());
List<Member> list = memberRepository.findByUsername("John", pageable);

list.forEach(System.out::println);

 

List 반환 타입 (정렬만 사용)

List<Member> findByUsername(String name, Sort sort);
Sort sort = Sort.by("username").ascending();
List<Member> list = memberRepository.findByUsername("John", sort);

list.forEach(System.out::println);

 

참고사항

  • Pageable과 Sort 생성: PageRequest.of(int page, int size, Sort sort)를 사용하여 Pageable 객체를 생성할 수 있습니다.
  • Page: 전체 페이지 수, 전체 요소 수, 현재 페이지의 콘텐츠 등을 포함하는 추가 메타데이터를 제공합니다.
  • Slice: 다음 페이지가 있는지 여부만 확인할 수 있으며, 전체 페이지 수와 요소 수에 대한 정보는 제공하지 않습니다.Slice (count X) 추가로 limit + 1을 조회한다. 그래서 다음 페이지 여부 확인(최근 모바일 리스트 생각해보면 됨)
  • List: 페이징 없이 정렬된 결과만 반환합니다.

 

 

다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.

  • 검색 조건: 나이가 10살
  • 정렬 조건: 이름으로 내림차순
  • 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {

    Page<Member> findByAge(int age, Pageable pageable);

}
@SpringBootTest
@Transactional
class MemberRepositoryTest {

    @Autowired
    private MemberRepository memberRepository;


    @Test
    public void page(){
        Member member1 = Member.builder().username("member1").age(10).build();
        Member member2 = Member.builder().username("member2").age(10).build();
        Member member3 = Member.builder().username("member3").age(10).build();
        Member member4 = Member.builder().username("member4").age(10).build();
        Member member5 = Member.builder().username("member5").age(10).build();

        memberRepository.save(member1);
        memberRepository.save(member2);
        memberRepository.save(member3);
        memberRepository.save(member4);
        memberRepository.save(member5);

        // when
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
        Page<Member> page = memberRepository.findByAge(10, 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부터 시작한다.

주의: Page1부터 시작이 아니라 0부터 시작이다.

 

 

 

카운트 쿼리 분리

복잡한 SQL 쿼리에서 데이터를 조회할 때와 카운트할 때 각각 다른 쿼리를 사용할 수 있습니다. 데이터 조회는 left join을 포함할 수 있지만, 카운트 쿼리는 성능을 위해 join을 생략할 수 있습니다.

 @Query(value = "select m from Member m",
        countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);​

카운트 쿼리 분리는 복잡한 SQL 쿼리에서 데이터 조회와 카운트 계산을 별도로 수행하여 성능을 최적화하는 방법을 의미합니다. 이는 특히 데이터 조회 시 여러 테이블을 조인해야 하는 경우에 유용합니다. 데이터 조회와 카운트 계산을 별도로 수행하면, 카운트 쿼리에서 불필요한 조인을 제거하여 성능을 향상시킬 수 있습니다.

전체 count 쿼리는 매우 무겁다.

Top, First 사용 참고

스프링 데이터 JPA에서는 Top 또는 First 키워드를 사용하여 상위 몇 개의 결과를 제한할 수 있습니다.

List<Member> findTop3By();`

 

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

Page<Member> page = memberRepository.findByAge(10, pageRequest);
 Page<MemberDto> dtoPage = page.map(m -> new MemberDto());