-
[Spring] @RequestParam과 @PageableDefault을 동시에 사용할 수 없는 이유백엔드/Spring 2025. 4. 5. 12:35반응형
아래 내용을 알고 있어야함
- DispatcherServlet의 동작 방식
회사에서 프론트엔드분이 Swagger 문서상에서는 페이지 요청에 필요한 Pageable 값이 필수값으로 적혀 있는데, 쿼리 파라미터 없이 사용해도 데이터를 받을 수 있으니 쿼리 파라미터 없이 요청해도 문제 없냐고 질문 받았다. (번외로 쿼리 파라미터는 size, page, sort인데 왜 Pageable이라고 적혀있는지도..)
그래서 필수값이 아닌 것을 명시하기 위해 @RequestParam(required=false)와 @PageableDefault을 동시에 사용하려고 했는데, 이러면 요청시 @PageableDefault이 무시되어 쿼리 파라미터 없이 요청하면 Pageable 값이 들어오지 않아 에러가 발생하는 것이었다.
내가 예상한 결과는 아래와 같았다
- @RequestParam(required=false)를 통해 Swagger에서는 문서상 필수값이 아님을 보여줌
- @PageableDefault(...)를 통해 쿼리 파라미터가 들어오지 않아도 default로 처리 되는게 있었으면 좋겠음
그래서 살펴보니 Spring MVC 동작 방식과 관련이 있었다.
https://maenco.tistory.com/entry/Spring-MVC-Argument-Resolver%EC%99%80-ReturnValue-Handler 검색해보니 중국어된 그림이 주로 나와서 그나마 유의미한 한글로 된 그림을 가지고 왔다.
사진에서 중요한 부분은 HandlerAdapter에서 ArgumentResolver를 선택하는 로직이 있는데, 구체적으로 설명하자면 아래 방식으로 동작한다. 이걸 스크린샷으로 하나하나 찍으면 분량이 너무 많으니 궁금한 분들은 아래 내용 보고 IDE 통해서 코드를 보면 된다.
1. 사진처럼 요청이 들어오고 HandlerAdapter를 호출한다
2. HandlerAdapter의 구현체인 RequestMappingHandlerAdapter에서 invokeHandlerMethod(...)를 실행한다
3. invokeHandlerMethod(...)에서 반환전 가장 마지막 코드인 invocableMethod.invokeAndHandle(...)를 호출한다
4. invocableMethod는 ServletInvocableHandlerMethod인데 이건 InvocableHandlerMethod를 상속한다.
5. ServleInvocableHanderMethod의 invokeAndHandle(...)는 InvocableHandlerMethod의 invokeForRequest(...)를 호출한다
6. InvocableHandlerMethod의 InvokeForRequest(...) 내부에서 getMethodArgumentValues(...)를 호출한다
7. InvocableHandlerMethod의 getMethodArgumentValues(...)는 HandlerMethodArgumentResolverComposite의 supportParameter(...)를 통해 매칭되는 ArgumentResolver가 존재하는지 확인한다.
8. 이후 InvocableHandlerMethod의 getMethodArgumentValues(...)는 HandlerMethodArgumentResolverComposite의 resolveArgument(...)를 통해 매칭되는 ArgumentResolver를 가져와서 처리한다
순서만 봐도 좀 복잡하게 되어있는데, 결론적으로는 7번, 8번 순서에서는 ArgumentResolver를 매칭하는 동작을 하고 있는데, 이때 이 매칭하는 결과값은 파라미터 하나에 ArgumentResolver와 관련된 어노테이션을 하나밖에 매칭하지 못한다.
여기에서 디버그 모드로 확인해보면 getArguementResolver가 두번씩 호출되는데 7번에서 한 번, 8번에서 한 번 호출된다.
이후에 궁금증이 들은 것은 왜 @ReuqestParam 대신 @PageableDefault가 무시될까를 생각해보니 Argument Resolver 저장방식 동작에도 고정된 순서가 있어서 @PageableDefault가 계속 무시되는게 아닐까라는 생각이 들었다.
RequestMappingHandlerAdapter에서 afterPropertiesSet() 메서드를 보니 ArgumentResolver의 순서가 정해져있었다.
처음 스프링 컨테이너가 실행될 때 @Bean, @Component로 되어 있는걸 전부 주입하고, 매핑을 하는데 위 사진처럼 다양한 것들이 세팅된다. 여기서 우리가 볼 곳은 첫 if문인 getDefaultArgumentResolver(...)이다.
여기 순서대로 ArgumentResolver가 추가 된다.
@RequestParam의 경우에는 첫번째로 등록되는 RequestParamMethodArgumentResolver로 등록이 된다.
@PageableDefault는 세가지 경우로 Resolver가 처리된다.
1) 아무것도 건들지 않았을 경우
spring-data-jpa 의존성이 있다면 SpringDataWebConfiguration이라는 곳에서 관련 Resolver를 자동으로 추가해준다
아래 사진에서 AuthArgumentResolver를 제외한 나머지 3개의 리졸버가 SpringDataWebConfiguration에서 등록된다.
2) PageableHandelrMethodArgumentResolver를 사용해 커스텀해서 Configuration에 등록할 경우
1번이 저장된 상황에서 2번이 저장된다면 1번과 충돌이 날테니 1번이 무시되고 커스텀한 리졸버 1개가 등록된다.
3) PageableHandlerMethodArgumentResolver를 커스텀 했으나 Configuration에 등록되지 않은 경우
커스텀을 만들었는데, 등록을 하지 않아도 1번이 무시된다. 그냥 PageableHandlerMethodArgumentResovler가 쓰이는 곳이 있으면 무조건 넘어가게 만든 것으로 보인다. Resolver 등록 로직 마지막에 ServletModelAttributeMethodProcessor가 있는데, 여기랑 연결이 되어서 Pageable과 바인딩이 되지 않아 에러가 발생한다.
ModelAttributeMethodProccessor 내부에 Binder가 존재함 별거 아닌 질문이었는데 너무 복잡하다
반응형'백엔드 > Spring' 카테고리의 다른 글
[Spring, JPA] 50% 확률로 통과하는 LocalDateTime 테스트 코드 버그 수정하기 (1) 2024.04.04 [JPA, MySQL] 같은 유저가 동시 접속을 해서 좋아요를 누르면 동시성 문제가..?? (1) 2023.08.15 [JPA, MySQL] 여러명이 동시에 좋아요를 누르면 데드락과 동시성 문제가..?? (1) 2023.08.15 [Spring, Test] 가독성 좋은 테스트 코드로 리팩터링하기 (0) 2023.08.13 [트랜잭션] @Transactional 전파 옵션, 프록시 패턴 트러블 슈팅 (0) 2023.08.07