순수 JPA 페이징과 정렬
JPA에서 페이징을 어떻게 수행하는지 알아 보자.
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")
.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();
}
JPA페이징 테스트 코드
@Test
void paging() {
//given
memberJpaRepository.save(new Member("member1", 10));
memberJpaRepository.save(new Member("member2", 10));
memberJpaRepository.save(new Member("member3", 10));
memberJpaRepository.save(new Member("member4", 10));
memberJpaRepository.save(new Member("member5", 10));
int age = 10;
int offset = 0;
int limit = 3;
//when
List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
long totalCount = memberJpaRepository.totalCount(age);
//페이지 계산 공식 적용
// totalPage = totalCount /size ...
// 마지막 페이지...
// 최초 페이지...
//then
assertThat(members.size()).isEqualTo(3);
assertThat(totalCount).isEqualTo(5);
}
스프링 데이터 JPA를 적용하지 않은 일반 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 조회) → 더보기 버튼을 누르면 데이터를 추가해서 보여주는 상황에 사용할 수 있다.
사용 예시
public interface MemberRepository extends Repository<Member, Long> {
Page<Member> findByAge(int age, Pageable Pageable);
}
- 반환 값을
Page<Member>
로 반환하고, 검색 조건값인 age와 페이징을 처리하기 위한 Pageable 인터페이스값을 파라미터로 넣어서 생성했다.
@Test
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
Page<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.of(int page, int size, Sort sort)
: 현재 페이지값, 페이지당 보여줄 데이터 개수, 정렬 옵션을 파라미터로 넣어 줬다.
- desc offset을 이용해서 정렬
- 아래 쿼리의 count는 Page로 반환하므로 JPA에서 인식해서 자동으로 TotalCount(전체 데이터 갯수)값을 조회 했다.
반환 타입 인터페이스
Page 인터페이스
// Page는 Slice를 상속 받고 있다.
public interface Page<T> extends Slice<T> {
int getTotalPages();//전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
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(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable();//다음 페이지 객체
Pageable previousPageable();//이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
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);
- 복잡한 sql에서 사용하여 최적화를 진행할 수 있으며, 데이터는 left join하고, count는 left join을 안하게 함으로써 최적화가 이루어 진다.
페이지를 유지하면서 DTO로 반환하기
Page<Member> page = memberRepository.findByAge(age, pageRequest);
Page<MemberDto> memberDtoPage = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
map()
을 이용해서 DTO로 변환 할 수 있다.
하이버네이트 6이상에서의 left join 자동 최적화
@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);
위의 코드의 JPQL에서는 left join을 사용하고 있으나, 하이버네이트6에서 left join을 제거하는 최적화를 실행한다.
위의 쿼리에서 member
와 team
을 조인하지만, select절이나, where절에서 team을 전혀 사용하지 않는다. 이는 사실상 member에 관한 쿼리와 같다. → select m from Member m
left join
을 사용하였으므로, 왼쪽의 member자체를 다 조회하게 되는데, 만약 select나 where에 team의 조건이 들어간다면, 정상적으로 join문이 출력되겠지만, 사용하지 않으므로 JPA에서 최적화를 진행해 join없이 SQL문을 만들어 준다.
만약 member와 team을 하나의 SQL로 한번에 조회하고 싶다면, JPA의 fetch join
을 사용하면 된다.
반응형