본문 바로가기

Spring boot/스프링 데이터 JPA

[spring boot] 페이징과 정렬

페이징과 정렬 파라미터

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

 

특별한 반환 타입

  • org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
  • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지 유무만 확인 가능 (내부적으로 limit+1 조회)
  • List (자바 컬렉션) : 추가 count 쿼리 없이 결과만 반환

 

페이징과 정렬 사용 예제

Page findByUsername(String name, Pageable pageable);   //total count 쿼리 사용

Slice findByUsername(String name, Pageable pageable);   //total count 쿼리 사용 안함

List findByUsername(String name, Pageable pageable);    //total count 쿼리 사용 안함

List findByUsername(String name, Sort sort);

 

 

페이지를 유지하면서 엔티티를 DTO로 변환하기

// test 
Page page = memberRepository.findByAge(10, pageRequest); 
Page dtoPage = page.map(m -> new MemberDto());

API로 반환하려면 무조건 entity가 아닌 DTO로 변환해야한다. 

 


 

Page ( Content + Total count query )

// 정렬, 페이징 repository
public interface MemberRepository extends Repository<Member, Long> {
	Page<Member> findByAge(int age, Pageable pageable);
}
// test 코드
@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.findByAge(10, pageRequest);
    
	//then
	List<Member> content = page.getContent(); //조회된 데이터
    	long totalElements = page.getTotalElements(); // total 카운트 쿼리
    
	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(); //다음 페이지가 있는가?
}
  • total count 쿼리에는 DESC등 없이 최적화된 코드가 자동으로 날라감.
  • PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다.
  • 두 번째 파라미터로 받은 Pagable 은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다.

Count 쿼리 최적화

  • 쿼리가 left join일때 카운트 쿼리도 left join하면 성능이 안 좋아지기 때문에 카운트 쿼리는 내가 정해줄 수 있다.
  • count 쿼리는 어차피 left join을 하나 안 하나 결과가 똑같지만 left join을 하면 매우 무거운 쿼리가 된다.
// 정렬, 페이징 repository
public interface MemberRepository extends Repository<Member, Long> {
    @Query(value = "select m from Member m left join m.tea t", 
           countQuery "select count(m) from Member m")
    Page<Member> findByAge(int age, Pageable pageable);
}

 


 

Slice ( Content + 다음 페이지 유무만 )

// 정렬, 페이징 repository
public interface MemberRepository extends Repository<Member, Long> {
	Slice<Member> findByAge(int age, Pageable pageable);
}
// test 코드
@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"));
	Slice<Member> page = memberRepository.findByAge(10, 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(); //다음 페이지가 있는가?
}

// assertThat에 // 한 이유는 getTotalElements와 getTotalPages가 Page에서만 가능한 기능이기 때문.
  • 쿼리는 요청한 사이즈보다 1개 더 요청한다. (사용 예. 1개 더 요청했을때 있으면 '더보기'버튼 누를 수 있도록, 없으면 '더보기' 버튼 없애기)

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); //변환기
}

 


 

List ( Content만 )

// test 코드
@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"));
	List<Member> page = memberRepository.findByAge(10, pageRequest);
    
	//then
	
}
  • 다른 쿼리 없이 정직하게 딱 요청한 사이즈만큼의 데이터만 가져오기

 

 

pageRequest 만들 때 쿼리 조건이 너무 복잡하면 repository @Query 속에 내가 직접 적어줄 수 있다.

단순하게 3건만 조회하고 싶다면 pageRequest 안 만들고

       - {Page, Slice, List 중 1} page = memberRepository.findTop3ByAge(age);

       - Top, first 참고 : Spring Data JPA - Reference Documentation

Page, Slice, List 모두 Page는 0부터 시작