스프링 데이터 JPA 페이징과 정렬

구본식·2023년 2월 6일
0

Spring Data JPA

목록 보기
4/8
post-thumbnail
post-custom-banner

페이징과 정렬

순수 JPA 코드에서 페이징 처리를 하려면 offset, limit를 사용하여 페이징 계산을 해야되는 복잡한 과정들이 있었다.

이러한 번거로운 작업을 스프링 데이터 JPA가 매우 편하게 사용할수 있도록 지원해준다.

페이징 기능과 정렬기능은 스프링 데이터 JPA국환된 기능이 아닌, 어떠한 DB든 상관없이 적용이 가능한 Springframwork Data stack이 제공해주는 기능이다!

페이징과 정렬기능을 사용하기 위해 보통 PageRequest라는 구현체 클래스를 사용하고 반환형으로 Page, Slice를 사용한다.


1. Page

오프셋 기반 페이징 처리를 할때 보통 사용된다. 페이지 번호가 존재하는 게시판과 같이
총 페이지수, 총 데이터 수등을 필요로 하는 페이징 처리에 사용된다.(count 쿼리 발생)

Spring Data JPA Repository

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

test 코드

    //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 JPAPage하기 위해서 총 페이지수, 총 데이터 수등의 정보를 알아야 하기 때문에, 내부적으로 count 쿼리을 날리기 때문이다!!!

또한 page를 하기위해서 추가한 정렬조건등은 count 쿼리에는 반영되지 않는다.
(나름 최적화된 쿼리이다!)

만약 별도의 조건문 없이 left join등을 사용한 복잡한 쿼리문이 있을때, count 쿼리도 해당 쿼리문을 그대로 실행시키면서 마지막에 데이터 개수를 카운트하게 된다..

하지만 count 쿼리가 left join등과 관계없이 특정 테이블의 개수만 카운터해도 되는 상황에서는 쓸데없는 쿼리문 때문에 성능이 떨어진다.

그래서 다음과 같이 count 쿼리를 분리할 수있는 방안도 제공해준다.


2. Page & count 쿼리 분리

count 쿼리를 분리할 수 있는 방법을 보여주겠다.

Spring Data JPA Repository

//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 쿼리는 무겁기 때문에 이를 통해 성능 최적화를 할 수있다!)

test 코드

	//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 쿼리가 실행되는것을 볼수 있다.

실무에서 성능을 위해서 중요한 부분이라고 한다. 나중에 직접 만나게 되면 이 개념을 적용해보아야 겠다.


3. Slice

No 오프셋 기반 페이징 처리를 할때 보통 사용된다. 무한 스크롤 방식이라고도 한다.
현대 웹 사이트나 모바일에서 페이지 번호없이 '다음보기'버튼이나 자동으로 다음 게시물들을 보여주는 방식등과 같이 총 페이지수, 총 데이터 수가 필요없는 환경에서 사용된다.
(count 쿼리 발생X)

Spring Data JPA Repository

//Spring Data JPA - 페이징과 정렬 -> slice 페이징과 정렬
Slice<Member> findSliceByAge(int age, Pageable pageable);

test 코드

 //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(); //다음 페이지 있는지
    }

앞서 말했듯이 SlicePage와 다르게 총 페이지수, 총 데이터수들이 필요없기 때문에
getTotalElements(), getTotalPages()의 기능이 제공되지 않는다!

또한 Slice는 count 쿼리를 사용하지 않고 데이터를 가져올때 limit+1을 통해 다음 데이터가 있는지 없는지를 확인한다.

profile
백엔드 개발자를 꿈꾸며 기록중💻
post-custom-banner

0개의 댓글