페이징 과제가 끝난지 한참 됐는데 아직도 페이징 공부를 전혀 안 했다.
일단 내일은 N+1 문제 강의를 듣고 JPA 공부 좀 하고 주말에 시간 내서 해야겠다.
오늘은 자세히 몰랐었던 ArgumentResolver에 대해 알아보려 한다.
과제에 있어서 좀 자세히 알아보게 되었다.

먼저 JSON 데이터가 JAVA 환경에서 사용되는 방법에 대해 알아보자.
HTTP Body 영역의 데이터를 InputStream 객체를 이용해 이진 데이터로 가지고 온 후,
인코딩 하고 문자열로 변환하면 JAVA 환경에서 사용 가능한 데이터 타입이 된다.
이후에 ObjectMapper 객체로 문자열로 표현된 JSON 데이터를 JAVA 객체로 생성하면 된다.

DispatcherServlet은 Controller의 메소드를 직접 실행하지 않는다.
🚨 왜냐?
Controller는 다양한 유형이 존재하므로 직접 의존하게 되면 다형성을 보장할 수 없기 때문이다.
그래서 중간에 HandlerAdapter를 두어 Controller 메소드 실행 권한을 위임하는 것이다.
가장 대표적인 HandlerAdapter는 RequestHandlerMappingAdapter이다.
얘는 @RequestMapping, @Controller로 선언된 Controller 메소드 실행 권한을 가진다.
Controller는 필요한 데이터를 파라미터로 요구한다.
@RequestBody, @ResponseBody, @ModelAttribute, @RequestParam 같은 어노테이션을 포함하여, HttpEntity 같은 클래스 타입도 요구에 포함된다.
HandlerAdapter는 메소드의 파라미터를 읽고, 이에 맞는 데이터를 Request 객체에서 추출하고 변환하여 파라미터로 넘겨준다.
응답 또한 마찬가지로 메소드의 반환 타입을 읽어 이에 맞는 변환 로직을 거친다.
Controller의 이런 다양한 파라미터를 모두 지원할 수 있는 이유는 ArgumentResolver 덕분이다.
ArgumentResolver의 대표적인 인터페이스는 HandlerMethodArgumentResolver이다.
이 인터페이스는 다음과 같이 2개의 메소드를 가진다.
✅ supportParameter()
- 해당 파라미터의 데이터를 생성해서 넘겨줄 수 있는 지 여부를 점검
- 여기서 true가 되어야 resolveArgument()가 실행됨
✅ resolveArgument()
- 파라미터로 넘겨질 데이터를 생성하여 Object 타입으로 반환
ArgumentResolver에서 파라미터에 명시된 데이터를 반환한다는 것을 알게 되었다.
@RequestBody 등 HTTP Body 영역에 있는 데이터를 파라미터에 명시된 타입으로 변환하여 전달할 것을 요구한다면 컨버팅이 필요하다.
이 과정을 담당하는 클래스가 바로 HttpMessageConverter이다.
ArgumentResolver는 HttpMessageConverter에게 HTTP Body 영역의 데이터를 파라미터에 맞게 변환하여 넘겨주기를 요청한다.
이제 이해를 위해 예를 들어보자.

클라이언트가 Spring MVC 기반 서버로 HTTP 요청을 보냈다.
HTTP Body에는 JSON 데이터가 담겨져 있고, Content-Type과 Accept 헤더는 application/json 정보가 담겨져있다.
즉, JSON 타입의 데이터를 보낼테니 응답도 JSON으로 달라는 의미이다.
DispatcherServlet은 URL과 매핑되는 Controller를 스프링 컨테이너에서 가져온다.
그리고 나서 Controller 실행 권한을 HandlerAdapter에게 위임한다.
메소드의 파라미터가 @RequestBody UserCreateRequest request 라고 해보자.
이는 HTTP Body 영역의 데이터를 UserCreateRequest로 변환하여 전달해달라는 요구이다.
파라미터에 적합한 데이터를 전달하는 ArgumentResolver는 HTTP Body 영역의 데이터를 변환해야 하므로, HttpMessageConverter의 구현체 중에 적합한 컨버터를 탐색한다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
}
@PostMapping("/login")
public void login(@Auth AuthUser user, @RequestBody UserUpdateRequest request) {
...
}
이렇게만 해주면, @Auth는 내가 만든 커스텀 어노테이션이므로, Spring이 제공하는 ArgumentResolver 구현체로는 AuthUser 객체를 생성해서 파라미터로 넘기지 못한다.
그렇기에 @Auth 어노테이션이 붙어있을 때 AuthUser 객체를 넘겨줄 수 있는 ArgumentResolver 구현체를 직접 만들어야 한다.
public class AuthUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 1. @Auth 어노테이션이 선언되어 있는가?
boolean hasAuthAnnotation = parameter.getParameterAnnotation(Auth.class) != null;
// 2. 파라미터가 AuthUser 클래스인가?
boolean isAuthUserType = parameter.getParameterType().equals(AuthUser.class);
// 3. @Auth 어노테이션과 AuthUser 타입이 함께 사용되지 않은 경우 예외 발생
if (hasAuthAnnotation != isAuthUserType) {
throw new AuthException("@Auth와 AuthUser 타입은 함께 사용되어야 합니다.");
}
return hasAuthAnnotation;
}
@Override
public Object resolveArgument(
@Nullable MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory
) {
// HttpServletRequest 객체로 형변환
HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest();
// JwtFilter 에서 set 한 userId, email, userRole 값을 가져옴
Long userId = (Long) request.getAttribute("userId");
String email = (String) request.getAttribute("email");
UserRole userRole = UserRole.of((String) request.getAttribute("userRole"));
return new AuthUser(userId, email, userRole);
}
AuthUserArgumentResolver는 HandlerMethodArgumentResolver 인터페이스의 구현체이다.
HandlerMethodArgumentResolver는 @RequestMapping이 선언된 컨트롤러를 지원하는 ArgumentResolver이다.
ArgumentResolver는 두 가지 메소드를 구현하면 된다.
HandlerAdapter는 파라미터에 객체를 전달할 수 있는 ArgumentResolver를 탐색 한다고 했는데, ArgumentResolver의 supportsParameter() 메소드를 호출하여 적합 여부를 판단한다.
supportsParameter()에서 true가 반환되면 resolveAgument()를 호출한다.
resolveAgument() 메소드는 실제로 파라미터에 넘길 객체를 생성하는 메소드이다.
마지막으로 커스텀 ArgumentResolver를 Spring이 사용할 수 있도록 등록해줘야 한다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new AuthUserArgumentResolver());
}
}
WebMvcConfigurer 인터페이스는 Spring이 제공하는 각종 인터페이스에 커스텀 된 구현체들을 쉽게 등록할 수 있도록 기능을 제공한다.
WebMvcConfigurer의 addArgumentResolvers() 메소드는 ViewResolver를 등록하는 기능을 제공하는 메소드이다.
오호.. 주말 공부라.. ^_^