본문 바로가기
JPA

[Spring Data JPA] Page, Slice

by 감자b 2024. 12. 29.

먼저 페이징을 위해선 메서드에서 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

 

 

 

'JPA' 카테고리의 다른 글

[Spring Data JPA] @EntityGraph  (0) 2024.12.30
[Spring Data JPA] @Modifying  (0) 2024.12.30
[JPA] OSIV(Open Session In View)  (1) 2024.12.29
[JPA] JPQL  (0) 2024.12.29
[JPA] 데이터 타입  (0) 2024.12.29