JPA

[Spring Data JPA] Page, Slice

감자b 2024. 12. 29. 02:50

먼저 페이징을 위해선 메서드에서 Pageable을 인자로 받아야 한다.

Pageable은 인터페이스로 페이징을 위한 정보를 저장하는데 대표적인 구현체인 PageRequest의 생성자를 보면 다음과 같다.

protected PageRequest(int page, int size, Sort sort) {
    super(page, size);
    Assert.notNull(sort, "Sort must not be null");
    this.sort = sort;
}
  • page : 페이지 번호 (0부터 시작)
  • size : 해당 페이지의 데이터의 개수
  • sort : 정렬 기준

Spring Data JPA는 페이징을 처리할 때 컨트롤러에서 파라미터로 인터페이스인 Pageable을 받을 수 있다.

@GetMapping("/members")
public Page<Member> list(Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;
}

위와 같이 사용하면 파라미터로 넘어온 정보를 가지고 PageRequest 객체를 생성해준다.


위에서 여기서 반환 타입을 살펴보면 Page로 반환되는 것을 알 수 있다.

JPA는 페이징을 위해 2가지 객체를 제공한다.

1. Page

추가 count 쿼리 결과를 포함하는 페이징 기법을 의미한다.

Slice를 상속받아 Slice의 모든 메서드를 사용할 수 있고 추가로 전체 페이지 수, 전체 데이터 수를 가져오는 메서드가 있다.

따라서 조회 쿼리 이후에 전체 데이터 개수를 조회하는 count 쿼리가 한 번 더 발생한다.

(참고로 전체를 조회하는 count 쿼리는 매우 무겁다.)

public interface Page<T> extends Slice<T> {
     int getTotalPages(); //전체 페이지 수
     long getTotalElements(); //전체 데이터 수
     <U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}

//Page<Member> findByUsername(String name, Pageable pageable);

만약 복잡한 sql의 경우 count 쿼리를 분리할 수 있다.

카운트 쿼리 분리할 때 데이터는 left join, 카운트는 left join 안해도 된다.

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

2. Slice

추가 count 쿼리 없이 다음 페이지만 확인 가능한 페이징 기법을 의미한다.

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<Member> findByUsername(String name, Pageable pageable); 

즉 Slice는 전체 데이터 개수는 알 수 없으며 이전, 다음 Slice의 존재 여부만 확인 가능하다.

→ 모바일에서 무한 스크롤 구현하는 경우 유용. Page에 비해 쿼리가 하나 덜 날아가므로 데이터 양이 많을수록 Slice를 사용하는 것이 유리하나 전체 페이지 개수나 데이터 개수가 필요한 웹 게시판 같은 경우 Page가 유용하다.


페이지 사이즈 지정

페이지 개수의 경우 글로벌 설정이나 개별 설정을 통해 지정할 수 있다.

  • 글로벌 설정
spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/
  • 개별 설정
@RequestMapping(value = "/members_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "username", direction = Sort.Direction.DESC) Pageable pageable) {
         ...
}

페이징 정보가 여러 개일 때

파라미터로 페이징 정보가 여러 개 넘어올 때 접두사로 구분이 가능하다.

@Qualifier(접두사명) → {접두사명}_xxx로 파라미터로 넘어올 때 해당 pageable이 적용

예시) /members?member_page=0&order_page=1

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

Page → DTO

Page는 map() 메서드를 통해 내부 데이터를 다른 것으로 변경 가능하다.

Page<Entity> page = ...
Page<Dto> pageToDto = page.map(x -> new Dto(...));

Page 1부터 시작

Spring Data JPA는 페이징 처리 시에 Page를 0부터 시작한다.

1부터 시작하려면 어떻게 해야할까?

 

1. spring.data.web.pageable.one-indexed-parameters: true

spring:
 data:
  web:
   pageable:
    one-indexed-parameters: true

위 방법은 web 컨트롤러에서 받는 page 파라미터를 -1 처리를 한다.

하지만 응답으로 받은 Page, Slice 객체를 보면 내부 데이터는 0으로 반환한다는 한계가 있다.

{
    "content": [
        ...
    ],
    "pageable": {
        "offset": 0,
        "pageSize": 10, 
        "pageNumber": 0 // 0 인덱스
    },
    "number": 0, // 0 인덱스
    "empty": false
}

 

2. Pageable, Page를 파라미터, 응답으로 사용하지 않고, 직접 클래스를 정의한다.

  • 그리고 직접 PageRequest를 생성해서 리포지토리에 넘긴다.
  • 응답의 경우도 Page 대신에 직접 정의해서 제공한다.
  • + 여러 글을 찾아보니 Spring MVC의 핸들러 메서드에서 페이지네이션을 처리하는 방식에 대한 사용자 정의 설정을 제공하는 역할인 PageableHandlerMethodArgumentResolverCustomizer를 빈으로 등록하는 방법이 있었다.
@Configuration
public class WebConfig {
    @Bean
    public PageableHandlerMethodArgumentResolverCustomizer customizer() {
        return pageableResolver -> {
            pageableResolver.setOneIndexedParameters(true); // page 요청이 1로 오면 0으로 인식
            pageableResolver.setMaxPageSize(500); // 최대 요청 가능한 size
            pageableResolver.setFallbackPageable(PageRequest.of(0, 10)); // page 정보가 없을 때 기본 페이지 설정
        };
    }
}

 

GET - http://localhost:8080/api/posts?page=1 요청 시 아래와 같이 page.number = 0으로 인식되는 것을 확인 가능하다.

{
    "isSuccess": true,
    "code": "200OK",
    "message": "성공입니다.",
    "result": {
        "posts": {
            "content": [
                {
                    "id": 1,
                    "title": "title",
                    "content": "content",
                },
                {
                    "id": 2,
                    "title": "title",
                    "content": "content",
                },
            ],
            "page": {
                "size": 10,
                "number": 0,
                "totalElements": 3,
                "totalPages": 1
            }
        }
    }
}

참고

 

실전! 스프링 데이터 JPA 강의 | 김영한 - 인프런

김영한 | 스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제

www.inflearn.com