1. 개요
Spring Boot로 페이지네이션 API를 구현할 때 보통, Pageable을 사용하여 간편하게 구현한다.
Pageable을 컨트롤러의 메서드의 인자로 선언할 때, Spring MVC는 요청 파라미터를 자동으로 해석하여 Pageable 객체를 주입해준다. 어떤식으로 동작하는지 자세히 살펴보자.
@RequiredArgsConstructor
@RequestMapping("/v1/articles")
@RestController
public class ArticleController {
@GetMapping
public ResponseEntity<ApiResponse<Page<SearchArticleResponse>>> getArticles(
@PageableDefault(size = 10, sort = "createdAt", direction = Sort.Direction.DESC)
Pageable pageable,
) {
// 구현 생략..
return ResponseEntity.ok().body(response);
}
}
2. 동작 과정
Spring MVC 구조를 모르는 사람을 위해 간단하게 짚고 넘어간다.
1. Spring MVC 구조
클라이언트가 요청을 보내면, 서블릿 컨테이너에 의해 DispatcherServlet.doService() 메서드로 요청을 전달한다
여기서, 요청을 처리할 핸들러(컨트롤러)를 찾고, 핸들러를 실행할 수 있는 HandlerAdapter를 탐색한다. 이 때, @RequestMapping 기반 컨트롤러이면 RequestMappingHandlerAdpater가 선택된다. RequestMappingHandlerAdapter는 InvocableHandlerMethod 객체를 생성하여 실제 요청을 컨트롤러에 전달한다.
InvocableHandlerMethods는 HandlerMethodArgumentResolverComposite를 가진다.
public class InvocableHandlerMethod extends HandlerMethod {
private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
// ..
}
2. HadnlerMethodArgumentResloverComposite 동작
HandlerMethodArgumentResolverComposite는 여러 HandlerMethodArgumentResolver 구현체를 리스트로 관리한다.
이 때, 성능을 위해 동일한 MethodParamter에 대해서는 Map으로 캐싱하여 관리한다.
특정, MethodParameter를 통해 적절한 HandlerMethodArgumentResolver를 찾았으면, 해당 Resolver를 실제 요청 파라미터를 파라미터를 객체(Object)로 변환한다.
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache =
new ConcurrentHashMap<>(256);
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
@Nullable
public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
}
3. HanlderMethodArgumentResolver(실제 Pageable로 변환)
MethodParameter가 Pageable인 경우 PageableHandlerMethodArgumentResolver가 Resolver로서 역할을 하며, 요청 파라미터를 파싱해서 PageRequest를 반환한다. PageRequest는 Pageable의 구현체이다.
public class PageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolverSupport implements PageableArgumentResolver {
@Override
public Pageable resolveArgument(MethodParameter methodParameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) {
String page = webRequest.getParameter(getParameterNameToUse(getPageParameterName(), methodParameter));
String pageSize = webRequest.getParameter(getParameterNameToUse(getSizeParameterName(), methodParameter));
Sort sort = sortResolver.resolveArgument(methodParameter, mavContainer, webRequest, binderFactory);
Pageable pageable = getPageable(methodParameter, page, pageSize);
if (sort.isSorted()) {
return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort);
}
return pageable;
}
}
aricles/?page=0&size=10&sort=creadeAt,DESC를
PageRequest로 변환하면 PageRequest(page=0, size=10, Sort.by("createdAt").descending())
으로 변환된다.
3. 정리
Spring MVC 동작과정에 의해 Pageable은 PageableHandlerMethodArgumentResolver가 자동으로 요청 파라미터를 파싱해 Pageable의 구현체인 PageRequest로 변환해준다.
'Programming > Spring' 카테고리의 다른 글
| [JPA] JPA Soft Delete 지원하는 여러 방식들에 대한 고민(@SoftDelete, @SqlDelete, @SQLRestriction) (0) | 2026.01.04 |
|---|---|
| [Spring] Filter vs Interceptor 차이 (0) | 2025.10.20 |
| [Spring Security] SecurityFilterChain 동작 분석 - Logout 요청 시 AuthenticationEntryPoint 미호출 문제 (3) | 2025.08.13 |
| [Spring 테스트] Mockito ArguementCaptor란? (1) | 2025.08.10 |
| [Spring 테스트] 테스트 간 데이터 충돌 문제 해결(@Sql 이해) (0) | 2025.07.11 |