STUDY/Spring

Spring Boot | PageRequest

개미606 2021. 8. 18. 15:50

Spring Data JPA를 사용해 페이징을 하려는데, 컨트롤러에서 바로 Pageable을 받고싶지는 않았다..

Pageable

사실 Pageable을 이용하면 정말 쉽게 바로 받아진다.

import org.springframework.data.domain.Pageable;

@RestController
@RequestMapping("api/v1/orders")
public class OrderController {

    @GetMapping()
    public Response getOrders(final Pageable pageable){
        // ...
    }
}

api/v1/orders?page=0&size=20&sort=id,desc이렇게 요청하면, 쿼리 스트링이 바로 pageable에 매핑된다.

PageRequest

하지만 Pageable인터페이스를 구현한 PageRequest라는 객체가 있다.

package org.springframework.data.domain;

import org.springframework.data.domain.Sort.Direction;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * Basic Java Bean implementation of {@link Pageable}.
 *
 * @author Oliver Gierke
 * @author Thomas Darimont
 * @author Anastasiia Smirnova
 * @author Mark Paluch
 */
public class PageRequest extends AbstractPageRequest {

    private static final long serialVersionUID = -4541509938956089562L;

    private final Sort sort;

 // 생략...

컨트롤러를 수정(pageable → pagerequest)한 다음,
동일하게 api/v1/orders?page=0&size=20&sort=id,desc 요청하면 기본 생성자가 없다는 에러가 발생한다.

No primary or single public constructor found for class org.springframework.data.domain.PageRequest

import org.springframework.data.domain.PageRequest;

@RestController
@RequestMapping("api/v1/orders")
public class OrderController {

    @GetMapping()
    public Response getOrders(final PageRequest pagerequest){
        // ...
    }
}

PageRequest를 만들자!

PageRequest가 있다는 걸 알게된 이상.. 컨트롤러에서 바로 Pageable을 사용하고 싶지는 않고..
그렇다고 각각 @RequestParam으로 받고 싶지도 않았다.


이 글 (Spring Data JPA를 활용한 페이징 API 만들기)을 참고해서 따로 PageRequest를 작성하기로 한다.

common패키지를 생성한 후 PageRequest클래스를 작성해주었다.

@Getter
public class PageRequest {
    private int page;
    private int size;
    private List<String> sort;

    public void setPage(int page) {
        this.page = page <= 0 ? 1 : page;
    }

    public void setSize(int size) {
        int DEFAULT_SIZE = 10;
        int MAX_SIZE = 50;
        this.size = size > MAX_SIZE ? DEFAULT_SIZE : size;
    }

    public void setSort(List<String> sort) {
        this.sort = sort;
    }
}

sort항목을 문자열로 받아서 다시 Sort객체로 변환해야 한다.

처음에는 &sort=fieldname,asc이런식으로 pageable사용법과 동일하게 하려다가,
requestParam은 중간에 콤마가 있으면 자동으로 배열이나 리스트로 변환해주기 때문에 sort항목이 하나만 올 경우 fieldname과 asc가 각각 나뉘어져 버리는 문제 때문에 콤마를 사용해 구분하지 않기로 했다.


콤마 대신 #을 이용해서 필드명과 정렬 방식을 구분하도록 했다.
PageRequest는 public생성자가 없기 때문에 of()메서드를 호출하여 반환해야 한다.
Pageable의 page는 0부터 시작하는데, 1부터 받고 있어서 -1 해주었음.

public org.springframework.data.domain.PageRequest of() {
    if (sort == null || sort.isEmpty()) {
        return org.springframework.data.domain.PageRequest.of(page - 1, size);
    }

    return org.springframework.data.domain.PageRequest.of(page - 1, size, Sort.by(getOrders(sort)));
}

private List<Sort.Order> getOrders(List<String> sort) {
    List<Sort.Order> orders = new ArrayList<>();
    sort.forEach(str ->
            orders.add(
                    new Sort.Order(Sort.Direction.valueOf(str.split("#")[1].toUpperCase(Locale.ROOT)),
                    str.split("#")[0])
            )
    );
    return orders;
}

sort항목의 값이 &sort=fieldname#desc와 같은 형식이 지켜지지 않으면 에러가 발생하므로, 유효성 검사를 해주기로 한다.

spring-boot-starter-validation가 필요하다.

private List<@Pattern(regexp = "\\w*#+(desc|DESC|asc|ASC)\\Z",
                            message = "정렬하고자 하는 필드 명과 정렬방향을 정확히 입력하세요.") String> sort;

이제 /api/v1/orders?page=1&size=20&sort=createdAt#desc&sort=name#asc이런 식으로 요청하면 된다!