본문 바로가기

Develop/SPRING FRAMEWORK

Spring Data JPA를 활용한 페이징과 정렬 기능 구현

스프링 데이터 JPA가 제공하는 파라미터반환 타입을 활용하여 페이징과 정렬 기능을 쉽게 구현할 수 있다.

 

1. 페이징과 정렬 파라미터

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

2. 반환 타입

  • org.springframework.data.domain.Page : 카운트 쿼리를 포함하는 페이징
  • org.springframework.data.domain.Slice : 카운트 쿼리를 포함하지 않고 다음 페이지 존재 여부만 확인 가능한 페이징
  • List : 카운트 쿼리를 포함하지 않고 결과만 반환

public interface MemberRepository extends JpaRepository<Member, Long> {
  Page<Member> findByAge(int age, Pageable pageable); // 테스트하는 메소드
  Slice<Member> findByAge(int age, Pageable pageable);
  List<Member> findByAge(int age, Pageable pageable);
  List<Member> findByAge(int age, Sort sort);
  ...
}
PageRequest pageRequest = PageRequest.of(0, 3, Sort.Direction.DESC, "username");
Page<Member> page = memberRepository.findByAge(10, pageRequest);

다음 코드에서 페이징 처리를 하기 위한 findByAge 메소드의 파라미터로 Pageable 인터페이스를 설정했다. 따라서 아래 테스트 코드에서는 PageRequest 객체를 생성하여 실제 구현체로 넘겨주었다. 해당 PageRequest 객체는 페이징에 필요한 조건들을 담고 있다. 또한 반환 타입이 Page<Member>로 설정되었기 때문에 카운트 쿼리를 통해 전체 페이지와 데이터의 수를 알 수 있다.

 

public interface MemberRepository extends JpaRepository<Member, Long> {
  Slice<Member> findByAge(int age, Pageable pageable);
  ...
}

만약 카운트 쿼리를 날릴 필요가 없다면 위처럼 반환타입을 Slice로 설정하면 된다. 이때 스프링 데이터 JPA는 내부적으로 limit+1개의 데이터를 조회하게 된다. 따라서 다음 페이지 여부를 확인할 수 있다. 예를 들어 모바일 화면에서 데이터를 조회할 때 스크롤 하단 부분에 더보기 버튼을 통해 다음 페이지를 조회하는 경우에 사용한다.

@Query(value = "select m from Member m join m.team t",
	countQuery = "select count(m.username) from Member m")
Page<Member> findMemberAllCountBy(Pageable pageable);

또한 다음과 같이 카운트 쿼리를 분리할 수도 있다. 실제로 전체 COUNT 쿼리는 굉장히 무겁다. 만약 카운트에서 사용되는 데이터에 join이 필요 없다면 이처럼 분리하여 성능을 최적화할 수 있다. (Page 인터페이스의 카운트 쿼리는 내부적으로 이렇게 동작)

 

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

물론 페이징과 정렬에서도 엔티티를 그대로 노출하는 건 피해야 한다.  따라서 DTO로 변환하여 반환한다. (이때도 페이지 유지 가능)


스프링 데이터가 제공하는 이러한 페이징과 정렬 기능을 스프링 MVC에서도 편리하게 사용할 수 있다.

// members?page=0&size=3&sort=id,desc&sort=username,desc처럼 호출 가능
@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
	Page<Member> Page = memberRepository.findAll(pageable);
    return page;
}

위와 같이 Controller의 파라미터로 Pageable 인스턴스를 받을 수 있다.

  • page: 현재 페이지 (0부터 시작)
  • size: 한 페이지에 나타낼 데이터 건수
  • sort: 정렬 조건
spring.data.web.pageable.default-page-size=20 // 기본 페이지 사이즈
spring.data.web.pageable.max-page-size=2000 // 최대 페이지 사이즈

만약 파라미터의 기본값을 글로벌하게 변경하고 싶다면 application.yml 파일 내에 해당 코드를 변경하면 된다.

@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username",
				direction = Sort.Direction.DESC) Pageable pageable) {
	...
}

또한 개별 컨트롤러의 페이징/정렬 기본값을 설정하고 싶다면 @PageableDefault 어노테이션을 사용하면 된다.

 

public String list(@Qualifier("member") Pageable memberPageable,
		@Qualifier("order") Pageable orderPageable) {
    ...
}

이때 만약 페이징 정보를 둘 이상으로 전달하고 싶다면 다음과 같이 접두사를 통해 구분하여 넘겨줘야 한다.

그러면 /members?member_page=0&order_page=1 처럼 여러 개의 페이징 정보를 동시에 전달할 수 있다.

 

마지막으로, 스프링 데이터가 제공하는 페이징 기능은 기본적으로 Page가 0부터 시작한다.

만약 이러한 Page를 1부터 시작하는 걸로 바꾸고 싶다면 아래와 같은 방법이 있다. (그냥 0부터 시작하는 게 깔끔한 거 같다..)

 

1. Pageable, Page를 사용하지 않고 직접 클래스를 만들어서 처리.

2. application.yml 내 spring.data.web.pageable.one-indexed-parameters: true로 설정.

 

그런데 2번 방법은 사실 눈속임이다. 요청 파라미터로 넘어오는 page를 -1 처리할 뿐이다. 따라서 데이터는 원하는대로 잘 넘어가지만 실제 반환되는 Page 객체 내의 필드들은 page가 0으로 설정되어있다. 사용하기 전 이러한 한계를 잘 고려해야 한다!