자바생
article thumbnail
728x90

0.1. JPA 페이징

 

<code />
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(); }
<java />
@Test void paging() throws Exception { //given memberJpaRepository.save(new Member("1번", 10)); memberJpaRepository.save(new Member("2번", 10)); memberJpaRepository.save(new Member("3번", 10)); memberJpaRepository.save(new Member("4번", 10)); memberJpaRepository.save(new Member("5번", 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 for (Member member : members) { System.out.println("member = " + member); } assertThat(members.size()).isEqualTo(3); assertThat(totalCount).isEqualTo(5); } /* member = Member(id=5, username=5번, age=10) member = Member(id=4, username=4번, age=10) member = Member(id=3, username=3번, age=10) */

 

0.1.1. limit는 있는데 왜 offset은 없나요?

JPA는 page가 0부터 시작하는데, 내부적으로 "최적화"가 되어 있어 offset이 0이면 쿼리에 보이지 않는다

만약에 offset이 1이라면 쿼리에 보일 것이다. 아래 쿼리는 offset을 1로 바꿨을 때이다

 

<sql />
select member0_.member_id as member_i1_0_, member0_.age as age2_0_, member0_.team_id as team_id4_0_, member0_.username as username3_0_ from member member0_ where member0_.age=10 order by member0_.username desc limit 3 offset 1;

 

0.1.2. totalCount는 왜 필요한가요?

totalCount는 데이터의 개수를 의미한다. 

즉, 총 페이지 개수나 몇 번째 페이지 를 알기위해 totalCount가 필요하다.

 

0.2. spring data JPA 페이징

spring data에서 페이징과 정렬을 제공하기 위해 Pageable, Sort 를 제공한다

  • 파라미터에 "Pageable"를 사용하면 반환타입을 Page, Slice 또는 List를 사용할 수 있다
    • Page를 사용하면 spring data jpa는 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다
    • Slice를 사용하면 Page와 다르게 count 쿼리 결과를 포함하지 않고, 다음 페이지만 확인할 수 있음 
      • 내부적으로 limit + 1 조회함

아래의 그림은 org.springframework.data.domain의 클래스 다이어그램을 일부이다

빨간색 상자는 앞으로 우리가 사용할 클래스나 인터페이스라고 생각하면 된다

 

0.3. Page

 

0.3.1. Page 객체는 어떻게 생성하나요?

 

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

Pageable을 파라미터로 사용하는데, 쿼리에 대한 조건들이 들어가있다고 생각하면 된다

 

Pageable을 구현하는 PageRequest 객체를 생성하여 파라미터로 넣어준다

<java />
@Test void paging() throws Exception { //given memberRepository.save(new Member("1번", 10)); memberRepository.save(new Member("2번", 10)); memberRepository.save(new Member("3번", 10)); memberRepository.save(new Member("4번", 10)); memberRepository.save(new Member("5번", 10)); int age = 10; PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); //when Page<Member> page = memberRepository.findByAge(age, pageRequest); //then List<Member> content = page.getContent(); for (Member member : content) { System.out.println("member = " + member); } 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(); }
  • page
    • 출력할 페이지를 나타냄
  • size
    • 데이터의 개수
  • sort
    • 정렬 조건

  • 앞서 말했듯이 Page를 사용하면 조회된 게시글 수를 count하는 쿼리가 나가는 것을 알 수 있다
  • Page와 Slice 인터페이스에서 제공하는 다양한 메서드를 통해 페이징 기능을 사용할 수 있다

 

0.3.2. Page 객체를 생성할 때 첫번째 파라미터인 page가 무엇인가요?

 

페이징을 했을 때, 지정된 page에 존재하는 데이터를 가져올 수 있는 파라미터이다

즉, 페이징을 하고 나서 특정 page에 존재하는 데이터를 가져올 수 있다

 

아래의 코드를 보면 1~9번까지의 member가 있다

이름을 기준으로 내림차순을 하게 되면 9,8,7 / 6,5,4 / 3,2,1 로 페이지가 구성이 된다

지정된 page 파라미터에 따라 조회되는 데이터가 달라짐을 알 수 있다

 

<java />
@Test void paging() throws Exception { //given memberRepository.save(new Member("1번", 10)); memberRepository.save(new Member("2번", 10)); memberRepository.save(new Member("3번", 10)); memberRepository.save(new Member("4번", 10)); memberRepository.save(new Member("5번", 10)); memberRepository.save(new Member("6번", 10)); memberRepository.save(new Member("7번", 10)); memberRepository.save(new Member("8번", 10)); memberRepository.save(new Member("9번", 10)); int age = 10; PageRequest pageRequest1 = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); PageRequest pageRequest2 = PageRequest.of(1, 3, Sort.by(Sort.Direction.DESC, "username")); PageRequest pageRequest3 = PageRequest.of(2, 3, Sort.by(Sort.Direction.DESC, "username")); //when Page<Member> page1 = memberRepository.findByAge(age, pageRequest1); Page<Member> page2 = memberRepository.findByAge(age, pageRequest2); Page<Member> page3 = memberRepository.findByAge(age, pageRequest3); //then List<Member> content1 = page1.getContent(); List<Member> content2 = page2.getContent(); List<Member> content3 = page3.getContent(); System.out.println("첫번째 페이지"); for (Member member : content1) { System.out.println("member = " + member); } System.out.println("두번째 페이지"); for (Member member : content2) { System.out.println("member = " + member); } System.out.println("세번째 페이지"); for (Member member : content3) { System.out.println("member = " + member); } } /* 첫번째 페이지 member = Member(id=9, username=9번, age=10) member = Member(id=8, username=8번, age=10) member = Member(id=7, username=7번, age=10) 두번째 페이지 member = Member(id=6, username=6번, age=10) member = Member(id=5, username=5번, age=10) member = Member(id=4, username=4번, age=10) 세번째 페이지 member = Member(id=3, username=3번, age=10) member = Member(id=2, username=2번, age=10) member = Member(id=1, username=1번, age=10) */

 

0.4. Slice

 

  • Page와 다르게 count 쿼리 없이 다음 페이지만을 확인한다
    • limit + 1 만큼을 조회한다
  • count 쿼리가 없기 때문에 Page에서 사용할 수 있었던 "getTotalPages", "getTotalElements" 메서드를 사용할 수 없다
<code />
Slice<Member> findByAge(int age, Pageable pageable);
<code />
@Test 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)); int age = 10; PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); //when Slice<Member> page = memberRepository.findByAge(age, 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(); }

PageRequest를 생성할 때 size를 3으로 지정했는데 limit 4 쿼리가 나간 것을 알 수 있다

  • Slice는 페이지가 총 몇개인지는 모르겠지만, 다음 페이지가 있어 없어 이 정도로 확인한다
  • 그래서 몇 번째 페이지까지 존재하느냐는 모르지만, 우리가 자주 사용하는 모바일 애플리케이션에서 "더보기"를 사용할 때 Slice를 사용할 수 있음

 

0.5. List

 

<code />
List<Member> findByAge(int age, Pageable pageable);
<code />
int age = 10; PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); //when List<Member> page = memberRepository.findByAge(age, pageRequest);

Page, Slice 뿐만 아니라 List로도 반환할 수 있다

 

0.6. 실무에서의 페이징

 

0.6.1. join을 이용하여 Page를 구현할 경우 count도 같이 join을 한다?

 

Page를 잘 안쓰려고 하는 이유는 count 쿼리가 나가게 된다

  • 즉, 예시를 보면 left outer join을 사용하게 되면 count도 left outer join을 사용함
    • left outer join을 사용할 경우 count도 굳이 left outer join할 필요가 없다
  • 따라서 복잡한 sql일수록 조인이 많이 일어나고, count도 조인을 같이 하게 되면 성능이 매우 안좋아짐
  • count 쿼리를 분리하여 해결할 수 있음

0.6.2. count 쿼리를 분리하지 않을 때

<java />
@Query(value = "select m from Member m left join m.team t") Page<Member> findByAge(int age, Pageable pageable);

<java />
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username")); Page<Member> page = memberRepository.findByAge(age, pageRequest);

 

 

0.6.3. count 쿼리를 분리하고 난 후

<code />
@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);

 

 

0.6.4. 페이지를 유지하며 Entity -> Dto 변환

 

우리는 앞서 Entity 자체를 반환하면 안되고 Dto로 변환하여 반환해야한다고 했다

 

Page에서는 내부적으로 map 메서드를 통해 Function 인터페이스를 인자로 받아 변환해줄 수 있다

<code />
<U> Page<U> map(Function<? super T, ? extends U> converter);

 

<code />
Page<Member> page = memberRepository.findByAge(age, pageRequest); Page<MemberDto> map = page.map(member -> new MemberDto(member.getId(), member.getUsername(), member.getTeam().getName()));

 


1. REFERENCES

실전! 스프링 데이터 JPA(김영한 님)

 

728x90

'Spring 강의 > Spring data JPA' 카테고리의 다른 글

@EntityGraph  (0) 2022.04.13
벌크 연산  (0) 2022.04.12
쿼리 메서드  (0) 2022.04.08
profile

자바생

@자바생

틀린 부분이 있다면 댓글 부탁드립니다~😀

검색 태그