순수 JPA 코드에서 페이징 처리를 하려면 offset, limit를 사용하여 페이징 계산을 해야되는 복잡한 과정들이 있었다.
이러한 번거로운 작업을 스프링 데이터 JPA
가 매우 편하게 사용할수 있도록 지원해준다.
페이징 기능과 정렬기능은 스프링 데이터 JPA
에 국환된 기능이 아닌, 어떠한 DB든 상관없이 적용이 가능한 Springframwork Data stack
이 제공해주는 기능이다!
페이징과 정렬기능을 사용하기 위해 보통 PageRequest
라는 구현체 클래스를 사용하고 반환형으로 Page
, Slice
를 사용한다.
오프셋 기반 페이징 처리를 할때 보통 사용된다. 페이지 번호가 존재하는 게시판과 같이
총 페이지수, 총 데이터 수등을 필요로 하는 페이징 처리에 사용된다.(count 쿼리 발생)
//Spring Data JPA - 페이징과 정렬
Page<Member> findPageByAge(int age, Pageable pageable);
//Spring Data JPA - 페이징과 정렬 -> @Query 레포지토리에 쿼리 작성
@Query("select m from Member m where m.username=:username and m.age>:age")
Page<Member> findMemberPage(@Param("username") String username, @Param("age") int age, Pageable pageable);
//Spring Data JPA - 기본 페이지과 정렬 테스트
@Test
public void page() 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));
//when
PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findPageByAge(10, pageRequest);
//then
List<Member> content = page.getContent();
Assertions.assertThat(content.size()).isEqualTo(3);
Assertions.assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
Assertions.assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
Assertions.assertThat(page.getTotalPages()).isEqualTo(2); //전페 페이지 번호
Assertions.assertThat(page.isFirst()).isTrue(); //첫번째 페이지 맞는지
Assertions.assertThat(page.hasNext()).isTrue(); //다음 페이지 있는지
}
//Spring Data JPA - 기본 페이지과 정렬 테스트 + 레포지토리에 쿼리 작성 사용
@Test
public void pageMember() throws Exception {
//given
memberRepository.save(new Member("member", 10));
memberRepository.save(new Member("member", 15));
memberRepository.save(new Member("member", 15));
memberRepository.save(new Member("member", 15));
memberRepository.save(new Member("member", 15));
//when
PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findMemberPage("member", 10, pageRequest);
//then
List<Member> content = page.getContent();
Assertions.assertThat(content.size()).isEqualTo(3);
Assertions.assertThat(page.getTotalElements()).isEqualTo(4); //전체 데이터 수
Assertions.assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
Assertions.assertThat(page.getTotalPages()).isEqualTo(2); //전페 페이지 번호
Assertions.assertThat(page.isFirst()).isTrue(); //첫번째 페이지 맞는지
Assertions.assertThat(page.hasNext()).isTrue(); //다음 페이지 있는지
}
page() 테스트
메서드 이름으로 쿼리생성
방식을 사용하였다.
해당 쿼리에 페이징,정렬를 위해서 Pageable
인터페이스를 전달해야하고, PageRequest
는 이 인터페이스의 구현체가 된다.
PageRequest
클래스에 page(페이지 번호), size(한페이지에 가져올 개수), Sort(여러개의 정렬조건 가능) 등의 페이징과 정렬에 필요한 파라미터를 설정할 수 있다.
쿼리의 결과로 Page
반환형으로 받을수 있고,
전체 데이터 수,현재 페이지 번호, 전체 페이지 번호 등 다양한 기능을 제공한다.
pageMember() 테스트
@Query를 사용한 레포지토리에 쿼리작성
방식을 사용하였다.Pageable
인터페이스를 파라미터로 전달하게 되면 스프링 데이터 JPA
가 페이징, 정렬 처리를 해주게 된다. 실제 테스트를 하고 발생한 쿼리문을 보게 되면 Page
를 하기 위해서 Spring Data JPA
가
별도의 count 쿼리도 자동으로 생성하여 발생하는것을 볼 수있다.
왜냐하면 앞서 말했듯이 Spring Data JPA
가 Page
하기 위해서 총 페이지수, 총 데이터 수등의 정보를 알아야 하기 때문에, 내부적으로 count 쿼리을 날리기 때문이다!!!
또한 page
를 하기위해서 추가한 정렬조건등은 count 쿼리에는 반영되지 않는다.
(나름 최적화된 쿼리이다!)
만약 별도의 조건문 없이 left join등을 사용한 복잡한 쿼리문이 있을때, count 쿼리도 해당 쿼리문을 그대로 실행시키면서 마지막에 데이터 개수를 카운트하게 된다..
하지만 count 쿼리가 left join등과 관계없이 특정 테이블의 개수만 카운터해도 되는 상황에서는 쓸데없는 쿼리문 때문에 성능이 떨어진다.
그래서 다음과 같이 count 쿼리를 분리할 수있는 방안도 제공해준다.
count 쿼리를 분리할 수 있는 방법을 보여주겠다.
//Spring Data JPA - 페이징과 정렬 -> count 쿼리를 다음과 같이 분리
@Query(value = "select new study.springdatajpa.dto.MemberDto(m.id, m.username, t.name) from Member m left join m.team t where m.username=:username",
countQuery = "select count(m) from Member m")
Page<Member> findMemberAndTeamPage(@Param("username") String username, Pageable pageable);
분리할 수 있는 방법을 해보기 위해 간단하게 left Join를 사용하여 쿼리를 작성해보았다.
본 쿼리는 left join를 포함한 쿼리가 나가지만, countQuery
는 별도로 작성한 count 쿼리문이 나가는 것을 볼 수 있다.
(전체 count 쿼리는 무겁기 때문에 이를 통해 성능 최적화를 할 수있다!)
//Spring Data JPA - 기본 페이지과 정렬 테스트 + count query 분리
@Test
public void pageMemberAndTeam() throws Exception {
//given
Team team1 = teamRepository.save(new Team("teamA"));
Team team2 = teamRepository.save(new Team("teamB"));
memberRepository.save(new Member("member", 10, team1));
memberRepository.save(new Member("member", 15, team1));
memberRepository.save(new Member("member", 15, team2));
memberRepository.save(new Member("member", 15));
memberRepository.save(new Member("member", 15));
//when
PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> page = memberRepository.findMemberAndTeamPage("member", pageRequest);
//then
List<Member> content = page.getContent();
Assertions.assertThat(content.size()).isEqualTo(3);
Assertions.assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
Assertions.assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
Assertions.assertThat(page.getTotalPages()).isEqualTo(2); //전페 페이지 번호
Assertions.assertThat(page.isFirst()).isTrue(); //첫번째 페이지 맞는지
Assertions.assertThat(page.hasNext()).isTrue(); //다음 페이지 있는지
}
위와 같은 간단한 테스트 코드를 통해 우리가 지정한 count 쿼리가 실행되는것을 볼수 있다.
실무에서 성능을 위해서 중요한 부분이라고 한다. 나중에 직접 만나게 되면 이 개념을 적용해보아야 겠다.
No 오프셋 기반 페이징 처리를 할때 보통 사용된다. 무한 스크롤 방식이라고도 한다.
현대 웹 사이트나 모바일에서 페이지 번호없이 '다음보기'버튼이나 자동으로 다음 게시물들을 보여주는 방식등과 같이 총 페이지수, 총 데이터 수가 필요없는 환경에서 사용된다.
(count 쿼리 발생X)
//Spring Data JPA - 페이징과 정렬 -> slice 페이징과 정렬
Slice<Member> findSliceByAge(int age, Pageable pageable);
//Spring Data JPA - 기본 페이지과 정렬 테스트 + slice
@Test
public void slice() 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));
//when
PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC, "username")); Slice<Member> page = memberRepository.findSliceByAge(10, pageRequest);
//then
List<Member> content = page.getContent();
Assertions.assertThat(content.size()).isEqualTo(3);
//Assertions.assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
Assertions.assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
//Assertions.assertThat(page.getTotalPages()).isEqualTo(2); //전페 페이지 번호
Assertions.assertThat(page.isFirst()).isTrue(); //첫번째 페이지 맞는지
Assertions.assertThat(page.hasNext()).isTrue(); //다음 페이지 있는지
}
앞서 말했듯이 Slice
는 Page
와 다르게 총 페이지수, 총 데이터수들이 필요없기 때문에
getTotalElements()
, getTotalPages()
의 기능이 제공되지 않는다!
또한 Slice
는 count 쿼리를 사용하지 않고 데이터를 가져올때 limit+1을 통해 다음 데이터가 있는지 없는지를 확인한다.