프로젝트를 진행하며 Pagination을 구현해야되는 상황이 생겼다.
이 때, 우리의 조건은 다음과 같았다.
- 정렬 기능은 기본적으로 생성일순 기준으로 합니다.
- 서치 기능에는 12 기준으로 페이지에 노출 될 수 있습니다. 이외의 건수는 제한하여 기본 12건씩으로 고정합니다.
기본적으로 Pagination은 다양한 요청 사항에서 공통적으로 적용될 수 있기에 코드의 중복을 줄이기 위한 방법을 고민하다 ArgumentResolver를 만들어 모두가 전역에서 편리하게 사용할 수 있도록 했다.
CustomPaginationArgument 개발
ArgumentResolver
ArgumentResolver에 대해 간략하게 설명을 해보자면, HandlerAdapter가 Handler를 호출할 때 필요한 파라미터를 생성해 넘겨주는 역할을 수행하는 것이 바로 ArgumentResolver이다.
즉 HandlerAdapter가 우리의 controller에서 어떤 파라미터가 필요하진 확인하고 그에 맞는 argumentResolver를 호출해 해당 파라미터를 컨트롤러를 호출하며 넘겨주는 방식이다.
예를 들어 우리가 흔히 Controller에서 @RequesetBody 를 통해 값을 가져온다면, 이때 HandlerAdapter는 우리 Controller에 @RequestBody 를 통해 값을 채우는 것을 확인하고 RequestBody에 관련된 ArgumentResolver를 호출해서 값을 채우게 된다. (실제 우리가 사용하는 @RequestBody의 argumentResolver의 역할을 하는 구현체는 RequestResponseBodyMethodProcessor이다.)
CustomPageableArgumentResolver 제작
위에서 예시가 @RequestBody였다면 실제 적용기에는 Pageable 타입을 파라미터로 넘겨받을 때이다.
개발을 할 때 페이징 처리를 위한 값들을 Pageable 형태로 값을 전달 받기로 결정했다.
@GetMapping
public ResponseEntity<ApiSuccessResponse<Page<GetOrderResponseDto>>> getOrders(
@RequestParam(required = false) Long userId,
@RequestParam(required = false) UUID storeId,
@RequestParam(required = false) OrderType type,
@RequestParam(required = false) OrderStatus status,
@RequestParam(required = false) OrderPaymentStatus paymentStatus,
@RequestParam(required = false) String show,
Pageable pageable) {
Page<GetOrderResponseDto> orderResponse = orderService.getOrders(userId, storeId, type, status, paymentStatus, show, pageable);
return ResponseEntity
.status(HttpStatus.OK)
.body(ApiSuccessResponse.of(
HttpStatus.OK,
"/api/orders?userId=" + userId + "&storeId=" + storeId + "&type=" + type + "&status=" + status
+ "&paymentStatus=" + paymentStatus + "&show=" + show,
orderResponse
));
}
위 코드는 실제로 주문 전체 조회를 구현하기 위한 Controller인데 이때 Pageable 타입을 파라미터로 넘겨 받는 것을 확인할 수 있다.
따라서 위에서 정한 제약 조건을 만족시키는 컴포넌트를 개발하기 위해서는 Pageable 타입의 파라미터를 생성해서 넘겨주는 PageableArgumentResolver를 개발하면 된다!
구현
먼저 ArgumentResolver를 구현하기 위해서는 ArgumentResolver의 인터페이스인 HandlerMethodArgumentResolver를 직접 구현하는 방식으로 진행하면 된다.
HandlerMethodArgumentResolver
public interface HandlerMethodArgumentResolver{
boolean supportParameter(MethodParameter parameter);
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
- support Parameter : 해당 리졸버가 어떤 메서드 파라미터를 지원하는지 여부이다.
- resolveArgument : 실제 데이터를 파싱해서 변환시켜 주는 부분이다.
1. supportParameter
여기서 만든 ArgumentResolver가 적용되는 메서드 파라미터는 바로 Pageable 타입이다. 따라서 전달 받은 메서드 파라미터가 Pageable과 같은 타입인지 검증하는 부분이다.
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Pageable.class.equals(parameter.getParameterType());
}
2. resolveArgument
전달 받은 값을 제약 조건에 받게 변경하는 부분이다.
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
int page = getPage(webRequest);
int size = getSize(webRequest);
Sort sort = getSort(webRequest);
if (page < 0) {
page = 0;
}
return new CustomPageRequest(page , size, sort);
}
private int getPage(NativeWebRequest webRequest) {
String page = webRequest.getParameter(PARAM_PAGE);
return (page != null) ? Integer.parseInt(page) -1 : 0;
}
private int getSize(NativeWebRequest webRequest) {
String size = webRequest.getParameter(PARAM_SIZE);
return size != null ? Integer.parseInt(size) : DEFAULT_PAGE_SIZE;
}
private Sort getSort(NativeWebRequest webRequest) {
String[] sortParams = webRequest.getParameterValues(PARAM_SORT);
if (sortParams == null || sortParams.length == 0) {
// 아무 sort 파라미터가 없으면 기본 정렬
return Sort.by(Sort.Direction.DESC, DEFAULT_SORT_FIELD);
}
List<Sort.Order> orders = new ArrayList<>();
for (String param : sortParams) {
if (param == null || param.isBlank()) continue;
// param 예시: "createdAt,desc"
String[] split = param.split(",");
String field = split[0];
// 방향값이 없으면 DESC로 기본 처리
String direction = (split.length > 1) ? split[1] : DEFAULT_SORT_DIRECTION;
Sort.Direction sortDirection = Sort.Direction.fromString(direction);
orders.add(new Sort.Order(sortDirection, field));
}
// 여러 Order 누적해서 Sort 생성
return Sort.by(orders);
}
입력 받은 page, size, sort, direction을 적절하게 수정하여 최종적으로 PageRequest 타입을 반환하게 된다.
3. 전체 코드
@Component
public class CustomPageableArgumentResolver implements HandlerMethodArgumentResolver {
private static final int DEFAULT_PAGE_SIZE= 12;
private static final String DEFAULT_SORT_FIELD = "createdAt";
private static final String DEFAULT_SORT_DIRECTION = "DESC";
private static final String PARAM_PAGE = "page";
private static final String PARAM_SIZE = "size";
private static final String PARAM_SORT = "sort";
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Pageable.class.equals(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
int page = getPage(webRequest);
int size = getSize(webRequest);
Sort sort = getSort(webRequest);
if (page < 0) {
page = 0;
}
return new CustomPageRequest(page , size, sort);
}
private int getPage(NativeWebRequest webRequest) {
String page = webRequest.getParameter(PARAM_PAGE);
return (page != null) ? Integer.parseInt(page) -1 : 0;
}
private int getSize(NativeWebRequest webRequest) {
String size = webRequest.getParameter(PARAM_SIZE);
return size != null ? Integer.parseInt(size) : DEFAULT_PAGE_SIZE;
}
private Sort getSort(NativeWebRequest webRequest) {
String[] sortParams = webRequest.getParameterValues(PARAM_SORT);
if (sortParams == null || sortParams.length == 0) {
// 아무 sort 파라미터가 없으면 기본 정렬
return Sort.by(Sort.Direction.DESC, DEFAULT_SORT_FIELD);
}
List<Sort.Order> orders = new ArrayList<>();
for (String param : sortParams) {
if (param == null || param.isBlank()) continue;
// param 예시: "createdAt,desc"
String[] split = param.split(",");
String field = split[0];
// 방향값이 없으면 DESC로 기본 처리
String direction = (split.length > 1) ? split[1] : DEFAULT_SORT_DIRECTION;
Sort.Direction sortDirection = Sort.Direction.fromString(direction);
orders.add(new Sort.Order(sortDirection, field));
}
// 여러 Order 누적해서 Sort 생성
return Sort.by(orders);
}
}
이때 Spring이 제공하는 pageable은 시작이 0페이지부터 시작하기에 나는 1페이지부터 시작하고 싶어, CustomPageRequest를 만들어 반환하도록 했다.
public class CustomPageRequest extends PageRequest {
protected CustomPageRequest(int pageNumber, int pageSize, Sort sort) {
super(pageNumber, pageSize, sort);
}
@Override
public int getPageNumber() {
return super.getPageNumber() + 1;
}
}
ArgumentResolver 추가
@Configuration
public class WebConfig implements WebMvcConfigurer {
private CustomPageableArgumentResolver pageableArgumentResolver;
public WebConfig(CustomPageableArgumentResolver pageableArgumentResolver) {
this.pageableArgumentResolver = pageableArgumentResolver;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(pageableArgumentResolver);
}
}
'백엔드' 카테고리의 다른 글
| Redis + Ip 기반 조회 수 어뷰징 방지 (0) | 2025.03.16 |
|---|---|
| 목록 조회 시 페이징 쿼리 최적화를 통한 최소 10배 성능 개선 (0) | 2025.03.12 |
| github packages를 통한 공통 모듈 개발기 (2) | 2025.02.13 |
| Spring Adapter-Handler를 흉내낸, 확장 가능성을 고려한 전략 패턴 기반 회원가입 컴포넌트 설계 (0) | 2025.02.13 |
| 대용량 트래픽에서 동시성 문제를 고려한 선착순 쿠폰 발급 처리 (0) | 2025.02.13 |