스프링 데이터 JPA - 페이징
순수 JPA 페이징 정렬
JPA에서 페이징을 어떻게 할 것인가?
다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.
- 검색 조건 : 나이가 10살
- 정렬 조건 : 이름으로 내림차순
- 페이징 조건 : 첫 번째 페이지, 페이지 당 보여줄 데이터는 3건
1
2
3
4
5
6
7
8
9
10
11
12
13
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();
}
페이징 쿼리랑 전체 수 쿼리를 만들었고 테스트 해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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));
memberJpaRepository.save(new Member("member6", 10));
int age = 10;
int offset = 0;
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(6);
}

통과 완료됐다. 아무튼 MyBatis 보다 훨씬 편하다.. DB만 바꿔도 hibernate 방언만 바꿔주면 되니깐..
스프링 데이터 JPA 페이징과 정렬
이제 스프링 데이터 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 쿼리 없이 결과만 반환.
주의 : 페이징 인데스는 1이 아니라 0이다.
자 이제 순수 JPA에서 했던 페이징을 다시 해보자
- 검색 조건 : 나이가 10살
- 정렬 조건 : 이름으로 내림차순
- 페이징 조건 : 첫 번째 페이지, 페이지 당 보여줄 데이터는 3건
MemberRepository.java
1
Page<Member> findByAge(int age, Pageable pageable);
자 이거 한 줄이 끝이다. 허허.. 토탈 카운트 이런 추가적인 것도 필요 없다.
offset, limit 이런 것도 Pageable 객체에 넣어주고 넘기면 된다.
테스트 코드를 보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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));
memberRepository.save(new Member("member6", 10));
// 페이징 해주는 객체
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
int age = 10;
//when
Page<Member> pages = memberRepository.findByAge(age, pageRequest);
// long totalCount = memberRepository.totalCount(age); 이것도 필요 없다.
// totalCount 도 같이 날린다.
//then
List<Member> content = pages.getContent();
long totalCount = pages.getTotalElements();
for (Member member : content) {
System.out.println(member);
}
System.out.println("totalCount = " + totalCount);
}
여기서 알아 두면 좋을 게
1
2
// 페이징 해주는 객체
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
다음과 같이 PageRequest.of() 메서드에 페이지 넘버, 사이즈, 소팅 방향, 조건 여러 개 ….
1
2
3
public static PageRequest of(int pageNumber, int pageSize, Sort.Direction direction, String... properties) {
return of(pageNumber, pageSize, Sort.by(direction, properties));
}
를 넣어주면 Pageable 객체로 사용할 수 있고
when 절에서 Page<Member> pages 이렇게 반환 받은 걸 볼 수 있다.
이제 테스트를 보면

쿼리 두 개가 심지어 최적화도 한 상태로 나간 걸 볼 수 있다…..
1
2
3
4
5
6
assertThat(content.size()).isEqualTo(3); // 가져온 데이터 수
assertThat(pages.getTotalElements()).isEqualTo(6); // 총 데이터 수
assertThat(pages.getNumber()).isEqualTo(0); // 시작 번호가 0 인가
assertThat(pages.getTotalPages()).isEqualTo(2); // 전체 페이지가 2 페이지인가
assertThat(pages.isFirst()).isTrue(); // 이게 첫 번째 페이지 인가.
assertThat(pages.hasNext()).isTrue(); // 다음 페이지가 있는가?
이런 테스트도 가능하다. 별의 별 기능을 다 지원해주네..
스프링 데이터 JPA - 슬라이스
이제 무한 스크롤 같은 데서 사용 할 수 있는 Slice를 한번 사용해 보자.
1
Slice<Member> findByAge(int age, Pageable pageable);

리포지토리에서 Slice로 반환 타입을 바꿔주고 테스트 코드를 Slice로 바꾸면 다음과 같이 된다. -> 전체 수 관련 된 것이 사용 할 수 없게 됐다.
무한 스크롤에서 전체 수가 필요한 건 아니고, 그 다음 내용이 있냐 없냐 정도만 필요하기 때문이다.

쿼리도 한 개만 나간다. 그런데 limit 가 몇 개 나갔을까? 우린 3개씩 자르기로 하긴 했다. 하지만.. p6spy로 보면

4 개가 나갔다. 즉 그 다음 내용이 있냐 없냐 정도만 필요하기 때문에 알아서 +1 해서 쿼리를 날린 것이다.
페이징 쿼리 totalCount 최적화.
실무에서는 전체 수를 가져오는 쿼리가 따로 다르게 할 수도 있다. 성능이나 조건 등을 따지면
그럴땐 리포지토리 단에서 @Query 를 사용하면 된다.
1
2
3
@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);
다음과 같이 데이터 가져올 땐 left join으로 팀 정보를 가져왔는데, 카운트 쿼리는 left join을 굳이 할 필요가 없기 때문에 (데이터 숫자는 같으니깐) 따로 빼 줄 수 가 있는 것이다.
추가적으로
1
2
Page<Member> pages = memberRepository.findByAge(age, pageRequest);
Page<MemberDto> toMap = pages.map(m -> new MemberDto(m.getId(), m.getUsername(), null));
컨트롤러 단에서 Page를 넘길 때 엔티티를 직접 넘기면 안되기 때문에 다음과 같이 변환도 가능하다. 이럼 Page특성도 그대로 사용하면서 DTO로 반환도 가능하다~
댓글남기기