순수 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부터 시작한다.
주의: Page는 1부터 시작이 아니라 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());
'JPA 실전' 카테고리의 다른 글
[JPA] JPA 실전 - 확장 기능 (0) | 2024.07.02 |
---|---|
[JPA] JPA 실전 - 쿼리 메소드 (3) (0) | 2024.07.02 |
[JPA] JPA 실전 - 쿼리 메소드 (1) (0) | 2024.07.02 |
[JPA] JPA 실전 - 공통 인터페이스 (1) | 2024.06.30 |
[JPA] JPA 실전 - 개발 기본 (0) | 2024.06.30 |