본문 바로가기

CS 잡지식

Spring Data JPA가 제공하는 막강한 [페이징],[정렬]

RDB이든, Mongo 같은 DB는 모두 아래 2개만으로 Spring Data 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 쿼리 없이 결과만 반환

public interface MemberReposiotry extends JpaRepository<Member,Long> {


    //이제는 Spring Data JPA로 [페이징]과 [정렬]을 구현해볼거다.
    Page<Member> findByAge(int age, Pageable pageable); // 쿼리 메서드의 이름으로 Spring Data JPA가 JPQL를 만들어 준다.


}

 

       
        // JPA는 첫 페이지가 1부터 시작, Spring Data JPA는 첫 페이지가 0부터 시작!!(주의!!)
        // [PageRequest]는 [page]인터페이스의 구현체!!!
        PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC,"username"));
        //위 코드에 의해 기대되는 페이징 결과 : page 0 : ["member1", "member2", "member3"] (총 3게)
        // page 1 : ["member4", "member5"] : 총 2개
        // -> 즉 Table에서 3개씩 묶어서 1개씩의 page를 만들어 준다.

        //page1,2의 총 튜플 개수 : 총 5개
        Page<Member> page = repository.findByAge(age, pageRequest); // findByAge()를 JPQL로 구현하지 않아도, 자동으로 생성해줌



        //위 코드에서 페이징 처리된 객체(3개)를 반환한다.
        List<Member> content = page.getContent();

        for (Member member : content) {
            System.out.println("member = " + member); //@ToString()이 toString()을 자동으로 오버라이딩해 줬음!!!
        }

        long totalCount = page.getTotalElements(); // totalCount 반환!
        System.out.println("totalCount = " + totalCount);

        assertThat(content.size()).isEqualTo(3);
        assertThat(page.getTotalElements()).isEqualTo(5);

        assertThat(page.getNumber()).isEqualTo(0); //페이지 번호(페이지 번호는 jpa든 Spring Data든 0부터 시작)
        // 1개의 페이지 내에서 시작한는 [튜플]의 [index]가 아닌, 2개의 page가 반환될 것인데, 그 페이지들 중 가장 첫번째 index(=0)을 의미!

        // 총 2개의 페이지가 반환된다(page 0, page 1)
         assertThat(page.isFirst()).isTrue();
         assertThat(page.hasNext()).isTrue();
    }

우리가 직접 JPQL을 작성하지 않았음에도, Spring Data JPA가 자동으로 메서드를 구현해 주었다. 

게다가 totalCount  정보도 생성해 준다(Page 인터페이스를 사용했을 때!)

public interface MemberReposiotry extends JpaRepository<Member,Long> {


    //이제는 Spring Data JPA로 [페이징]과 [정렬]을 구현해볼거다.
    // 1] Page 인터페이스 : 쿼리용 SQL문 1번, totalCount 쿼리용 SQL문 1번 : 총 2번의 쿼리 발생(실행해서 SQL문 확인!게시물에서 참조)
    Page<Member> findPageByAge(int age, Pageable pageable); // 쿼리 메서드의 이름으로 Spring Data JPA가 JPQL를 만들어 준다.

    // 2] Slice 인터페이스
     Slice<Member> findSliceByAge(int age, Pageable pageable);

     //  totalCount 계산은 부하가 크다. 왜냐하면, DB Table 전~체를 다 조회해야 하기 때문인데, 상황에 따라서 totalCount 계산을 최적화 해줘야 한다.
     // 우리가 쿼리 문을 날리게 되면, 대게의 경우, JOIN연산이 일어난다.
     // 여기서는 Member -> Team( 다 : 1 )과 left Outer Join으로 예시를 들겠다.
     // 만약, 2개의 Table 사이에 left outer join이 일어나게 된다면, 그 결과의 table의 totalcount나 member table의 total count나 값이 같다.
     // -> 이러한 점들을 이용해서 Spring Data JPA는 @Query( countQuery = " select ~~~ " ) 식으로 totalcount용 쿼리를 따로 짤 수 있게 해놓음
     @Query(value = "SELECT m FROM Member m LEFT JOIN m.team t",  // 원래는 여기에서 totalCount 쿼리 계산도 같이 일어남!
             countQuery = "SELECT COUNT(m.username) FROM Member m") // 이 부분에서 left outer Join으로 totalCount를 계산하지 않고,
                                                                    // Member Table 하나만을 가지고, totalCount를 계산한다. 결과는 똑같이 나온다.
                                                                    // 실행해서 sql문 확인해봐라(countQuery가 있고, 없고의 SQL문 차이 확인)
       Page<Member> findtotalCountByAge(int age, Pageable pageable);
}

 

    @Test@Transactional
    public void pagin(){

        /**
         *  1. Page 사용 예시
         */

        /*repository.save(new Member("member1",10));
        repository.save(new Member("member2",10));
        repository.save(new Member("member3",10));
        repository.save(new Member("member4",10));
        repository.save(new Member("member5",10));

        int age =10;

        // PageRequest는 인터페이스인 Pageable의 구현체!!!
        // JPA는 첫 페이지가 1부터 시작, Spring Data JPA는 첫 페이지가 0부터 시작!!(주의!!)
        // [PageRequest]는 [page]인터페이스의 구현체!!!
        PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC,"username"));


        // 쿼리 메서드용 SQL문 1번, totalCount용 SQL문 1번 : 총 2번의 쿼리리
       Page<Member> page = repository.findByAge(age, pageRequest); // findByAge()를 JPQL로 구현하지 않아도, 자동으로 생성해줌


        //위 코드에서 페이징 처리된 객체(3개)를 반환한다.
        List<Member> content = page.getContent();

        for (Member member : content) {
            System.out.println("member = " + member); //@ToString()이 toString()을 자동으로 오버라이딩해 줬음!!!
        }

        long totalCount = page.getTotalElements(); // totalCount 반환!
        System.out.println("totalCount = " + totalCount);

        assertThat(content.size()).isEqualTo(3);
        assertThat(page.getTotalElements()).isEqualTo(5);

        assertThat(page.getNumber()).isEqualTo(0);

         assertThat(page.isFirst()).isTrue();
         assertThat(page.hasNext()).isTrue();*/

        /**
         * 2. Slice 사용 예시 : 이건 [페이징] 기능을 하는 것이 전혀 아니다.
         * -> 다른 곳에서 [페이징] SQL문을 날린 후, 1개의 튜플들을 더 들고 오는 것에 불과하다.
         */

       /* repository.save(new Member("member1",10));
        repository.save(new Member("member2",10));
        repository.save(new Member("member3",10));
        repository.save(new Member("member4",10));
        repository.save(new Member("member5",10));

        int age =10;

        // [페이징] 요청은 여기서 일어남.
        PageRequest pageRequest = PageRequest.of(0,3, Sort.by(Sort.Direction.DESC,"username"));

        // 참고로, Slice 클래스는 PageRequest 클래스보다 더 상위(부모 or 조상 클래스)이기에, findPageByAge()의 반환형이
        // Page(정확히는 PageRequest)여도 업캐스팅돼서, 받을 수는 있다.
        // 내부적으로 limit [ + 1 ]을 하여 1개의 튜플을 추가적으로 더 들고 온다.
        Slice<Member> page = repository.findPageByAge(age, pageRequest); // findPageByAge()를 JPQL로 구현하지 않아도, 자동으로 생성해줌

        List<Member> content = page.getContent();

        for (Member member : content) {
            System.out.println("member = " + member); //@ToString()이 toString()을 자동으로 오버라이딩해 줬음!!!
        }

    //    long totalCount = page.getTotalElements();  Slice에는 이런 기능이 없다.
    //    System.out.println("totalCount = " + totalCount);

        assertThat(content.size()).isEqualTo(3);

      //assertThat(page.getTotalElements()).isEqualTo(5); Slice에는 이런 기능이 없다.

        assertThat(page.getNumber()).isEqualTo(0);

        //assertThat(page.getTotalPages()).isEqualTo(2); Slice에는 이런 기능이 없다.

        // 총 2개의 페이지가 반환된다(page 0, page 1)
        assertThat(page.isFirst()).isTrue();
        assertThat(page.hasNext()).isTrue();
*/
        /**
         *  Slice가 사용되는 예
         *  -> 모바일 디바이스에서 보면 아래로 쭉 스크롤을 하다가, [더보기]란이 있다.
         *  이거의 원리는 처음에 1개의 페이지를 보여주고, Slice가 만약 DB에 1개의 TUPLE이 추가로 남아 있으면 그것을 들고 온다.
         *  그렇게 추가의 튜플이 있으면, [더보기]란을 띄워주고, 없으면, 안 띄워 준다.
         *  -> 만약 Page로 했는데, totalCount가 너무 많으면, Slice 방식을 이용하여 최적화하는 방법도 있다.
         */



        repository.save(new Member("member1",10));
        repository.save(new Member("member2",10));
        repository.save(new Member("member3",10));
        repository.save(new Member("member4",10));
        repository.save(new Member("member5",10));

        int age =10;


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


        // 쿼리 메서드용 SQL문 1번, totalCount용 SQL문 1번 : 총 2번의 쿼리문 날림
        // Page는 outer Join을 사용하여 totalCount를 계산한다.
        // 그러나 만약 table이 100개 라면 수많은 join이 일어나면서 성능에 문제가 생긴다.
        // findPageByAge()를 @Query(countQuERY = "SELECT COUNT() ~~ "으로 JOIN 연산 없이 최적화하였다.
        // 실행 결과를 확인해라(게시물에서도 확인 가능)
        Page<Member> page = repository.findtotalCountByAge(age, pageRequest); // findByAge()를 JPQL로 구현하지 않아도, 자동으로 생성해줌

        // [도메인 엔티티]는 절대 그대로 클라이언트에게 반환을 하면 안되고, DTO를 만들어서 변환을 해줘야 한다.
        // -> 실무의 꿀팁이라며, map()을 사용하여 [paging을 유지하면서] 손쉽게 DTO로 변환하는 법도 있다고 하였다.
        Page<MemberDto> map = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
        // 이젠 Dto로 변환을 했으니, API 결과로서 반환해도 된다. 
       
       List<Member> content = page.getContent();

        for (Member member : content) {
            System.out.println("member = " + member); //@ToString()이 toString()을 자동으로 오버라이딩해 줬음!!!
        }

        long totalCount = page.getTotalElements(); // totalCount 반환!

        System.out.println("totalCount = " + totalCount);

        assertThat(content.size()).isEqualTo(3);

        assertThat(page.getTotalElements()).isEqualTo(5);

        assertThat(page.getNumber()).isEqualTo(0);

        assertThat(page.isFirst()).isTrue();
        assertThat(page.hasNext()).isTrue();

    }

 

@Query(countQuery)를 사용하지 않았을 때!

@Query(countQuery)를 사용하지 했을 때!