본문 바로가기

학습/TIL

Spring boot Pageable 유지하며 Restrict Sort 구현하기

❗문제 : Spring boot 에서 Pageable 을 통해 page, size, order 등 페이징에 대한 정보를 받아 오려고 하는데, sort 의 property 입력이 어려워요!

테스트 해보겠습니다.

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RequestMapping(value = "/pageable")
@RestController
public class PageableController {

    @GetMapping
    public void getWithPageable(Pageable pageable) {
   
    // Page Number : 0, Page Size : 20, Sort : UNSORTED
        log.info("Page Number : {}, Page Size : {}, Sort : {}",
                pageable.getPageNumber(),
                pageable.getPageSize(),
                pageable.getSort());
    }
}

http://localhost:8080/pageable

Pageable 에는 기본(Default) PageNumber, PageSize, Sort 가 있다.

 

 

그렇다면 Pageable 은 어떻게 파라미터를 받을까?

테스트 해보겠습니다.

<http://localhost:8080/pageable?page=2&size=35>
-> Page Number : 2, Page Size : 35, Sort : UNSORTED

이로써 Pageable 의 PageNumber 는 page, PageSize 는 size 라는 것을 알 수 있다.

Sort 는 어떻게 받을까?

Pageable 인터페이스에는 Sort getSort(); 가 정의되어 있다. 위의 Sort 객체를 찾아가보면 List<Order> 를 필드로 가지며 Order 는 Direction 과 property 로 이루어져 있는 것을 알 수 있다.

테스트 해보겠습니다.

<http://localhost:8080/pageable?page=2&size=35&sort=nickname,ASC>
-> Page Number : 2, Page Size : 35, Sort : nickname: ASC

<http://localhost:8080/pageable?page=2&size=35&sort=nickname,ASC&sort=phone,DESC>
-> Page Number : 2, Page Size : 35, Sort : nickname: ASC,phone: DESC

<http://localhost:8080/pageable?page=2&size=35&sort=create,name>
-> Page Number : 2, Page Size : 35, Sort : create: ASC,name: ASC

이로써 Pageable 의 Sort 는 sort 라는 파라미터 명의 , 로 구분한 property,direction 형식이라는 것을 알 수 있다.

 

아까 궁금했던 문제를 살펴보자.

 

Spring boot 에서 Pageable 을 통해 page, size, order 등 페이징에 대한 정보를 받아 오려고 하는데, 

sort 의 property 입력이 어려워요!

 

저기서 입력이 어렵다는 말은 property 는 String 을 사용하여 parameter 를 받고,

입력받은 property 를 가지고 Query 문에서 Select 하기 때문이다.

 

무슨말인지는 코드를 보자

import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequiredArgsConstructor
@RequestMapping(value = "/pageable")
@RestController
public class PageableController {

    private final PageableService pageableService;

    @GetMapping
    public void getWithPageable(Pageable pageable) {
        pageableService.retrieve(pageable);
    }
}

@RequiredArgsConstructor
@Service
public class PageableService {
    private final PageableRepository pageableRepository;
    
    public void retrieve(Pageable pageable){
        pageableRepository.findAll(pageable);
    }
}

 

위 코드와 같이 클라이언트에서 직접 받은 파라미터를 Select 문에 바로 사용한다고 하면,

 

실제 사용할 Sort Property 와 DB 의 이름이 다르다.

 

이처럼 클라이언트가 어떤 테이블의 어떤 파라미터로 Sort 할 것인지를 알아야 한다는 점이다.

이 포인트를 해결 할 인터페이스 를 정의 해봤다.

  1. 클라이언트가 보내 줄 Sort Property 를 정의한다.
  2. 전달 받은 Sort Property 를 검색하려는 테이블에 맞는 String 으로 변화한다.
@Getter
public enum RestrictPageableProperty {
    USER_NAME("user.name"),
    COMPANY_PHONE("company.phone"),
    ;

    private final String tableProperty;

    RestrictPageableProperty(String tableProperty) {
        this.tableProperty = tableProperty;
    }

    private static final Map<String, RestrictPageableProperty> nameMap = Arrays.stream(RestrictPageableProperty.values())
            .collect(Collectors.toUnmodifiableMap(RestrictPageableProperty::name, Function.identity()));

    public static RestrictPageableProperty get(String name) {
        return nameMap.get(name);
    }
}

 

1. 위에서 테스트한 Pageable 인터페이스 중 Sort 파라미터를 처리하는 SortHandlerMethodArgumentResolver 를 모방한 Resolver

public class RestrictPageableSortArgumentResolver extends SortHandlerMethodArgumentResolverSupport implements SortArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return Sort.class.equals(parameter.getParameterType());
    }

    @Override
    public Sort resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
        String[] directionParameter = webRequest.getParameterValues(this.getSortParameter(parameter));
        if (directionParameter == null) {
            return this.getDefaultFromAnnotationOrFallback(parameter);
        } else {
            return directionParameter.length == 1 && !StringUtils.hasText(directionParameter[0]) ? this.getDefaultFromAnnotationOrFallback(parameter) : this.parseParameterIntoSort(Arrays.asList(directionParameter), this.getPropertyDelimiter());
        }
    }

    private Sort parseParameterIntoSort(List<String> source, String delimiter) {
        List<Sort.Order> allOrders = new ArrayList<>();
        for (String part : source) {
            if (part != null) {
                allOrders.add(getOrder(part, delimiter));
            }
        }
        return allOrders.isEmpty() ? Sort.unsorted() : Sort.by(allOrders);
    }

    private Sort.Order getOrder(String sortByQuery, String delimiter) {
        String[] splitByDelimiter = sortByQuery.split(delimiter);
        final String property = splitByDelimiter[0];
        final String direction = splitByDelimiter.length == 1 ? Sort.Direction.ASC.name() : splitByDelimiter[1];
        return RestrictPageableOrder.of(property, direction).toOrder();
    }

}

 

💡 이 Resolver 는 기본 Pageable 핸들링을 위한 Sort Resolver 로 등록 해줘야 함

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new PageableHandlerMethodArgumentResolver(new RestrictPageableSortArgumentResolver()));
    }
}

 

2. Select 를 위한 Table Property 버전의 Sort 를 가져온다.

@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public class RestrictPageableOrder {
    private final RestrictPageableProperty property;
    private final Sort.Direction direction;

    public static RestrictPageableOrder of(String property,
                                           String direction) {
        return new RestrictPageableOrder(
                RestrictPageableProperty.get(property.toUpperCase()),
                Sort.Direction.valueOf(direction.toUpperCase())
        );
    }

    private boolean unknownOrder() {
        return Objects.isNull(direction) || Objects.isNull(property);
    }

    public Sort.Order toOrder() {
        if (unknownOrder()) {
						throw new RuntimeException();
        }
        return new Sort.Order(direction, property.getTableProperty());
    }

}

 

이렇게 만들어 놓고 테스트를 해보자

<http://localhost:8080/pageable?page=2&size=35&sort=nickname,ASC&sort=phone,DESC>
-> **ERROR** ( 정의 되지 않은 Property 를 사용하면 에러가 난다. )

<http://localhost:8080/pageable?page=2&size=35&sort=USER_NAME,ASC>
-> Page Number : 2, Page Size : 35, Sort : user.name: ASC

<http://localhost:8080/pageable?page=2&size=35&sort=USER_NAME,ASC&sort=COMPANY_PHONE,DESC>
-> Page Number : 2, Page Size : 35, Sort : user.name: ASC,company.phone: DESC

위 테스트와 같은 결과를 얻을 수 있다.

이로써 RestrictPageableResolver 의 역할은 두가지가 되었다.

  1. Sort 할 수 있는 Table Property 를 알 필요 없이 정해진 Enum 에 대해서만 요청하면 된다.
  2. 정해진 Enum 으로 제한하여 지원할 Sort Property 를 정의 할 수 있다.

읽어주셔서 감사합니다.