자바생
article thumbnail
728x90

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", 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();
}
@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)
*/

 

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

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

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

 

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;

 

totalCount는 왜 필요한가요?

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

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

 

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의 클래스 다이어그램을 일부이다

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

 

Page

 

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

 

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

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

 

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

@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 인터페이스에서 제공하는 다양한 메서드를 통해 페이징 기능을 사용할 수 있다

 

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

 

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

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

 

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

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

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

 

@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)
*/

 

Slice

 

  • Page와 다르게 count 쿼리 없이 다음 페이지만을 확인한다
    • limit + 1 만큼을 조회한다
  • count 쿼리가 없기 때문에 Page에서 사용할 수 있었던 "getTotalPages", "getTotalElements" 메서드를 사용할 수 없다
Slice<Member> findByAge(int age, Pageable pageable);
@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를 사용할 수 있음

 

List

 

List<Member> findByAge(int age, Pageable pageable);
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로도 반환할 수 있다

 

실무에서의 페이징

 

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

 

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

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

count 쿼리를 분리하지 않을 때

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

PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

Page<Member> page = memberRepository.findByAge(age, pageRequest);

 

 

count 쿼리를 분리하고 난 후

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

 

 

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

 

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

 

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

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

 

Page<Member> page = memberRepository.findByAge(age, pageRequest);

Page<MemberDto> map = page.map(member -> new MemberDto(member.getId(), member.getUsername(), member.getTeam().getName()));

 


REFERENCES

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

 

728x90

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

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

자바생

@자바생

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

검색 태그