오봉이와 함께하는 개발 블로그
스프링 데이터 JPA - 스프링 데이터 JPA 페이징과 정렬 본문
스프링 데이터 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 쿼리 없이 결과만 반환
Page
는 보통 우리가 아는 1 ~ n으로 이뤄진 페이지를 구현할 때 사용하고, Slice
는 모바일 환경에서 스크롤에 따라 자동으로 데이터가 노출되거나, 버튼(예 : 더보기 버튼)을 통해 페이징을 할 때 주로 사용한다.
페이징을 사용하지 않고, 데이터만 원하는 만큼 반환받고 싶을 때는 일반적인 List
로 반환 받으면 된다.
Pageable
, Page
등 인터페이스가 개발된 이후 개발자는 JPQL을 직접 짜더라도 페이징이나 totalCount
같은 외적인 관심사를 제외하고 집중해야 하는 쿼리만 짜면 된다.
페이징과 정렬 사용 예제
Page<Member> findByUsername(String name, Pageable pageable); // count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); // count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); // count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort);
- 조건
- 검색 조건 : 나이가 10살
- 정렬 조건 : 이름으로 내림차순
- 페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
Page 인터페이스
public interface Page<T> extends Slice<T> {
int getTotalPages(); // 전체 페이지 수
long getTotalElements(); // 전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); // 변환기
}
Page 사용 예제 코드
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findPageByAge(int age, Pageable pageable);
}
@Test
public void paging() 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))
// page 1 = offset = 0, limit = 10
// page 2 = offset = 10, limit = 10
// ..........
int age = 10;
// PageRequest는 Pageable의 구현체다.
// PageRequest.of(page, size, sorting 조건);
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"))
// when
Page<Member> page = memberRepository.findPageByAge(age, pageRequest);
// 반한 타입이 Page라서 totalCount가 필요한 것을 알기 때문에 totalCount 쿼리를 같이 날려서 totalCount 쿼리는 필요 없다
// 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
생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다.
여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다.
참고로 페이지는 0부터 시작한다.
두 번째 파라미터로 받은 Pagable
은 인터페이스다.
따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest
객체를 사용한다.
Page는 1부터 시작이 아니라 0부터 시작이기 때문에 주의하자.
Slice 인터페이스
Slice
는 totalCount
를 가져오지 않기 때문에 다음 페이지가 있는지, 없는지에 대한 여부로 페이징을 한다.
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(); // 이전 페이지 여부
Pageable getPageable(); // 페이지 요청 정보
Pageable nextPageable(); // 다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
Slice 사용 예제 코드
public interface MemberRepository extends JpaRepository<Member, Long> {
Slice<Member> findSliceByAge(int age, Pageable pageable);
}
@Test
public void paging() 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));
// page 1 = offset = 0, limit = 10
// page 2 = offset = 10, limit = 10
// ...........
int age = 10;
// PageRequest는 Pageable의 구현체다.
// PageRequest.of(page, size, sorting 조건);
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
// when
Slice<Member> slice = memberRepository.findSliceByAge(age, pageRequest);
// then
List<Member> contentSlice = slice.getContent(); //조회된 데이터
assertThat(contentSlice.size()).isEqualTo(3); //조회된 데이터 수
// slice에는 구현되지 않음 assertThat(slice.getTotalElements()).isEqualTo(5); //전체 데이터 수
assertThat(slice.getNumber()).isEqualTo(0); //페이지 번호
// slice에는 구현되지 않음 assertThat(slice.getTotalPages()).isEqualTo(2); //전체 페이지 번호
assertThat(slice.isFirst()).isTrue(); //첫번째 항목인가?
assertThat(slice.hasNext()).isTrue(); //다음 페이지가 있는가?
}
List 사용 예제 코드
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findListByAge(int age, Pageable pageable);
}
@Test
public void paging() 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));
// page 1 = offset = 0, limit = 10
// page 2 = offset = 10, limit = 10
// ...........
int age = 10;
// PageRequest는 Pageable의 구현체다.
// PageRequest.of(page, size, sorting 조건);
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
// when
List<Member> list = memberRepository.findListByAge(age, pageRequest);
// then
}
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 ?
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;
실무에서 중요한 점 1
페이징 쿼리를 잘 사용하지 않는 이유는 totalCount
쿼리가 DB에 있는 모든 데이터를 카운트 해야하기 때문에 성능이 좋지 않아서다.
where문을 사용하지 않는 가정하에 left join
을 하게 될 때, 데이터 갯수의 결과는 join을 하나, 하지 않으나 똑같기 때문에 left join
에 대한 totalCount
는 필요가 없어서 쿼리를 잘 짜야한다.
다음과 같이 count 쿼리를 분리할 수 있다.
@Query(value = “select m from Member m left join m.team”,
countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);
실무에서 중요한 점 2
PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
@Query(value = "select m from Member m order by m.username desc")
정렬 조건이 너무 복잡하면 Pageable(PageResult
)에서 잘 작동하지 않기 때문에 @Query
어노테이션을 이용해서 JPQL로 정렬 조건을 작성하자.
실무에서 중요한 점 2
Page<Member> page = memberRepository.findPageByAge(age, pageRequest);
엔티티는 외부에 노출시키면 안 되기 때문에 위 코드로 API 응답을 하면 안 된다.(위 코드는 엔티티임)
페이지를 유지하면서 엔티티를 DTO로 변환해서 응답하자.
Page<MemberDto> dtoPage = page.map(m -> new MemberDto(m.getId(), m.getUsername(), m.getTeam().getName()));
위 코드로 응답하면 MemberDto
로 응답하기 때문에 반환해도 되고, 내부 메소드를 통해 JSON으로 데이터가 잘 반환된다.
인프런 김영한 지식공유자님 강의 : 실전! 스프링 데이터 JPA
'BE > JPA' 카테고리의 다른 글
스프링 데이터 JPA - @EntityGraph (2) | 2022.09.13 |
---|---|
스프링 데이터 JPA - 벌크성 수정 쿼리 (0) | 2022.09.13 |
스프링 데이터 JPA - 순수 JPA 페이징과 정렬 (0) | 2022.09.13 |
스프링 데이터 JPA - 반환 타입 (0) | 2022.09.12 |
스프링 데이터 JPA - 파라미터 바인딩 (0) | 2022.09.08 |