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

2022. 7. 31. 02:32·공부/JPA
반응형

출처 : 인프런 실전! 스프링 데이터 JPA

Spring Data JPA 게시글은 대부분 인프런의 김영한님의 강의인 '실전! 스프링 데이터 JPA' 기반으로 내용을 정리했습니다.

 

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

페이징과 정렬 파라미터

  • org.springframework.data.domain.Sort : 정렬 기능
  • org.springframework.data.domain.Pageable : 페이징 기능(내부에 Sort 포함)

패키지명에서 알 수 있는 건 JPA를 사용하지 않는다는 것이다. 어떤 DB(관계형, noSql 등)를 사용하던 이 기능을 사용할 수 있도록 공통화시킨 것이다. 세부 구현은 물론 개발자가 알아서 직접 개발해야 한다.

 

특별한 반환 타입

  • org.springframework.data.domain.Page
    • 추가 count 쿼리 결과를 포함하는 페이징
    • totalCount가 필요할 때 사용
  • org.springframework.data.domain.Slice
    • 추가 count  쿼리 없이 다음 페이지만 확인 가능
    • 내부적으로 limit + 1 조회
    • 무한 스크롤 페이징에 좋음
  • List (자바 컬렉션) : 추가 count 쿼리 없이 결과만 반환

 

 

Page 인터페이스

public interface Page<T> extends Slice<T> {
   static <T> Page<T> empty() {
      return empty(Pageable.unpaged());
   }

   static <T> Page<T> empty(Pageable pageable) {
      return new PageImpl<>(Collections.emptyList(), pageable, 0);
   }

   int getTotalPages(); // 전체 페이지 수

   long getTotalElements(); // 전체 데이터 수

   <U> Page<U> map(Function<? super T, ? extends U> converter); // 변환기
}

 

page 예제

  • MemberRepository.interface
Page<Member> findByAge(int age, Pageable pageable);
  • MemberRepositoryTest.class
@Test
public void paging() {
    // 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;
    //** 1
    PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

    // when
    //** 2
    Page<Member> page = memberRepository.findByAge(age, pageRequest);
    //** 3
    //long totalCount = memberRepository.totalCount(age);

    // then
    List<Member> content = page.getContent(); // 데이터 꺼내기
    long totalElements = page.getTotalElements(); // totalCount

    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(); // 다음 페이지 있는지?
}
  1. Page는 1부터가 아닌 0부터 시작이므로 주의해야 한다.
  2. findByAge의 두 번째 파라미터는 Pageable 인터페이스이다. Pageable의 구현체인 PageRequest 객체를 사용하여 넘긴다. 주로 PageRequest 구현체를 사용한다.
  3. totalCount는 굳이 작성하지 않아도 된다. 반환 타입이 Page일 경우 스프링 데이터 JPA에서 알아서 totalCount 쿼리까지 한 번에 날려준다.
// 페이징 쿼리
	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=? 
    order by
        member0_.username desc limit ?
        
// totalCount 쿼리
    select
        count(member0_.member_id) as col_0_0_ 
    from
        member member0_ 
    where
        member0_.age=?

offset이 페이징 쿼리에서 보이지 않는 이유는 저번 게시글에서도 말했지만 offset 값이 0이기 때문에 굳이 넣지 않아도 되기 때문이다. totalCount 역시 order by절이 보이지 않는데, 이 이유는 totalCount에는 굳이 정렬할 필요가 없으니까 성능 최적화 상 뺀 것이다.

 

 

Slice 인터페이스

public interface Slice<T> extends Streamable<T> {
   int getNumber(); // 현재 페이지
   int getSize(); // 페이지 크기
   int getNumberOfElements(); // 현재 페이지에 나올 데이터 수
   List<T> getContent(); // 조회된 데이터
   boolean hasContent(); // 조회된 데이터 존재 여부
   Sort getSort(); // 정렬 정보
   boolean isFirst(); // 현재 페이지가 첫 페이지인지 여부
   boolean isLast(); // 현재 페이지가 마지막 페이지인지 여부
   boolean hasNext(); // 다음 페이지 여부
   boolean hasPrevious(); // 이전 페이지 여부
   default Pageable getPageable() { // 페이지 요청 정보
      return PageRequest.of(getNumber(), getSize(), getSort());
   }
   Pageable nextPageable(); // 다음 페이지 객체
   Pageable previousPageable(); // 이전 페이지 객체
   <U> Slice<U> map(Function<? super T, ? extends U> converter); // 변환기
}

Slice는 totalPage를 이용하지 않고 그냥 다음 페이지가 있어? 없어? 기준으로 작동되는 기능이다.

 

Slice 예제

  • MemberRepository.interface
//    Page<Member> findByAge(int age, Pageable pageable);
    Slice<Member> findByAge(int age, Pageable pageable);

참고로 위 코드를 수정하지 않으면 Page로 적용되어 totalCount가 발생한다. 이 이유는 Page의 부모가 Slice이기 때문에 테스트 코드에서 Slice만 수정하면 Page로 적용된다.

  • MemberRepositoryTest.class
@Test
public void paging() {
    // 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); //** Slice로 변경

	// 아래 totalCount와 관련된 메서드는 작동되지 않아 주석처리
    // then
    List<Member> content = page.getContent(); // 데이터 꺼내기
//        long totalElements = page.getTotalElements(); // totalCount

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

}
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=? 
order by
    member0_.username desc limit ?

쿼리는 totalCount와 관련된 쿼리는 발생되지 않는다. 여기서 주의 깊게 봐야 할 것은 출력 로그에 찍힌 select 쿼리의 limit 값이다. 예제에서는 0~3번째의 데이터, 즉 3개를 가져오라 명령했지만 select 쿼리가 나갈 때는 요청한 데이터의 +1 개를 가져온다. 즉 4개를 가져오게 된다.

 

List 예제

List 타입 반환의 경우 페이징이 주된 목적이 아니라, 그냥 데이터를 몇 번째부터 몇 번째까지 가져와가 목적이므로 페이징과 관련된 기타 기능들은 작동하지 않는다. 고로 totalCount 쿼리도 발생하지 않는다.

  • MemberRepository.interface
List findByAge(int age, Pageable pageable);
  • MemberRepsoitoryTest.class
@Test
public void paging() {
    // 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
    List page = memberRepository.findByAge(age, pageRequest);

	// 아래 메서드는 전부 동작하지 않는다.
    // then
//        List<Member> content = page.getContent(); // 데이터 꺼내기
//        long totalElements = page.getTotalElements(); // totalCount

//        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(); // 다음 페이지 있는지?
}
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=? 
order by
    member0_.username desc limit ?
! totalCount를 정하는 건 PageRequest 구현체가 아니고 반환 타입에 따라 결정된다.

 

 

Count Query 분리

totalCount는 DB에 있는 모든 데이터를 조회한다. 그렇기 때문에 totalCount 자체가 성능이 좋지 않고 무겁다. 그래서 totalCount의 쿼리를 잘 짜야할 경우가 있는데.. 특히 Join이 들어가는 경우에 신경을 많이 써야 한다. 만약 Member와 Team을 left outer join일 경우 totalCount는 굳이 join 할 필요가 없다. 왜냐하면 join을 하기 전이나, 하고 난 후나 데이터의 개수는 똑같기 때문이다(where절이 없는 경우에 한해). 

- 참고

 

[SQL] LEFT JOIN, INNER JOIN 차이

웹에서 게시판을 만들어보게 되면서 DataBase를 접하게 되었다. 각 주제에 따라 테이블을 나누어 작성한 후, 정보를 가져오는 경우 테이블을 합치는 경우가 있었다. 이때 JOIN을 사용하게 되는데,

velog.io

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

@Query 애노테이션을 추가해서 Member와 Team을 left join 해보자.

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_ 
left outer join
    team team1_ 
        on member0_.team_id=team1_.team_id 
order by
    member0_.username desc limit ?

// count 
select
    count(member0_.member_id) as col_0_0_ 
from
    member member0_ 
left outer join
    team team1_ 
        on member0_.team_id=team1_.team_id

그러면 count 쿼리는 굳이 join할 필요가 없는데도 join을 하면서 성능을 좋지 않게 만들고 있다.

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

@Query 안에 countQuery라는 속성을 따로 주면 CountQuery를 따로 지정할 수 있다.

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_ 
left outer join
    team team1_ 
        on member0_.team_id=team1_.team_id 
order by
    member0_.username desc limit ?

// count query
select
    count(member0_.member_id) as col_0_0_ 
from
    member member0_
! 참고로 Sort도 조건이 너무 복잡하면 PageRequest 구현체로 해결할 수가 없다. 그럴 경우엔 여기에서 작성한 것처럼 @Query의 value 속성에 sort 조건을 넣으면 된다.

 

 

DTO 변환

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

참고로 예제에서 위와 같이 Member 엔티티를 직접 가져와서 사용했는데, 실무에서 이런 식으로 사용하면 절대 안 된다. 엔티티는 절대 외부에 노출시키면 안 되고, 애플리케이션 안에 숨겨놓고 DTO로 변환해서 사용해야 한다.

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

이런 식으로 Page를 유지하면서 MemberDto로 변환할 수 있고 API로 반환해도 된다.

반응형
저작자표시 비영리 변경금지 (새창열림)
'공부/JPA' 카테고리의 다른 글
  • Spring Data JPA - @EntityGraph
  • Spring Data JPA - 벌크성 수정 쿼리
  • Spring Data JPA - 순수 JPA 페이징과 정렬
  • Spring Data JPA - 파라미터 바인딩, 반환 타입
데부한
데부한
어차피 할 거면 긍정적으로 하고 싶은 개발자
    반응형
  • 데부한
    동동이개발바닥
    데부한
  • 전체
    오늘
    어제
    • 분류 전체보기 (307)
      • 방통대 컴퓨터과학과 (27)
        • 잡담 (9)
        • 3학년1학기 (17)
      • 프로젝트 및 컨퍼런스 회고 (1)
        • 프로젝트 (4)
        • 한이음 프로젝트 (0)
        • 회고 (3)
      • 공부 (165)
        • Spring (37)
        • JPA (71)
        • 인프런 워밍업 클럽_BE (10)
        • Java (6)
        • React.js (27)
        • 넥사크로 (11)
        • 기타 (3)
      • 알고리즘 (85)
        • 알고리즘 유형 (10)
        • 알고리즘 풀이 (57)
        • SQL 풀이 (18)
      • 에러 해결 (13)
      • 잡담 (7)
        • 국비교육 (2)
        • 구매후기 (5)
        • 진짜 잡담 (0)
  • 블로그 메뉴

    • Github
    • Linkedin
    • 홈
    • 방명록
    • 글쓰기
    • 관리
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    react
    egov
    SQL
    코딩테스트
    백준
    스프링부트
    IT
    프로그래머스
    에러해결
    Spring
    개발자
    운영체제
    기출문제
    넥사크로
    자바스크립트
    알고리즘
    방통대
    전자정부프레임워크
    SpringBoot를 이용한 RESTful Web Service 개발
    RESTful
    springboot
    프론트엔드
    QueryDSL
    토이프로젝트
    MSA
    Java
    인프런
    토비의스프링부트
    JPA
    oracle
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
데부한
Spring Data JPA - 스프링 데이터 JPA 페이징과 정렬
상단으로

티스토리툴바