Springboot MVC 파헤치기(8) @ModelAttribute, @RequestParam, @PathVariable @ResponseBody @RequestBody 동작 과정
@ModelAttribute: 클라이언트가 전달하는 값을 객체로 맵핑해주는 역할을 합니다. HTTP Body 데이터 혹은 HTTP 파라미터를 주입합니다. 이때 생성자나 Setter로 주입하기 때문에 Setter혹은 생성자가 있어야 합니다.
@RequestParam: 클라이언트가 전달하는 HTTP 요청 Parameter 혹은 HTTP Body의 정보를 전달받기 위해서 사용됩니다.
@PathVariable: @RequestMapping의 URI의 경로 변수를 넣어주는 역할을 합니다.
@RequestBody: HTTP Body에 담겨온 정보를 JSON 형식으로 변환하여 객체와 맵핑해주는 역할을 합니다.
@ResponseBody: HTTP Body에 정보를 전달하기 전에 객체를 JSON 형식으로 변환하여 보내주는 역할을 합니다.
동작 과정
5개의 어노테이션 모두 별도로 개발자가 값을 꺼내오지 않고 바인딩이 됩니다. 이런 게 가능한 이유는
RequestHandlerMappingAdapter가 동작하며, 알맞은 ArgumentResolver가 존재한다면 동작합니다. 이러한 ArgumentResolver들은 HandlerMethodArgumentResolver를 상속받은 구현체를 통해서 동작합니다. Custom ArgumentResolver를 만들고 싶다면 상속받고 구현하시면 사용할 수 있습니다.
RequestHandlerMappingAdapter란?
- @RequestMapping 어노테이션을 이용해서 사용자의 요청을 처리할 수 있는 Controller와 맵핑해주는 역할을 합니다.
ArgumentResolver란?
- 위에서 봤듯이 컨트롤러에 들어온 요청 정보를 가공하여 값을 자동으로 넣어주는 역할을 합니다.
RequestHandlerMappingAdapter
spring container가 초기화될 때 생성되는 init Resolver들과 사용자가 만든 CustomResolver를 가지고 있습니다.
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30); // Annotation-based argument resolution resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false)); resolvers.add(new RequestParamMapMethodArgumentResolver()); resolvers.add(new PathVariableMethodArgumentResolver()); resolvers.add(new PathVariableMapMethodArgumentResolver()); resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new SessionAttributeMethodArgumentResolver()); resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RedirectAttributesMethodArgumentResolver()); resolvers.add(new ModelMethodProcessor()); resolvers.add(new MapMethodProcessor()); resolvers.add(new ErrorsMethodArgumentResolver()); resolvers.add(new SessionStatusMethodArgumentResolver()); resolvers.add(new UriComponentsBuilderMethodArgumentResolver()); if (KotlinDetector.isKotlinPresent()) { resolvers.add(new ContinuationHandlerMethodArgumentResolver()); } // Custom arguments if (getCustomArgumentResolvers() != null) { resolvers.addAll(getCustomArgumentResolvers()); } // Catch-all resolvers.add(new PrincipalMethodArgumentResolver()); resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true)); resolvers.add(new ServletModelAttributeMethodProcessor(true)); return resolvers; }
- ModelAttribute, PathVarialbe, RequestParam, RequestBody 등등 기본적인 ArgumentResovler들이 담겨있습니다.
- customArgumentResovler 또한 등록이 가능합니다.
RequestMappingHandlerAdapter에서 ArgumentResolver 동작 과정
필요한 부분만 주석으로 설명하겠습니다.
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); try { WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); if (this.argumentResolvers != null) { invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); //ArgumentResolver 사용 준비 하기 위해 넣는다. } if (this.returnValueHandlers != null) { invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } invocableMethod.setDataBinderFactory(binderFactory); invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); modelFactory.initModel(webRequest, mavContainer, invocableMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); asyncWebRequest.setTimeout(this.asyncRequestTimeout); WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); asyncManager.setTaskExecutor(this.taskExecutor); asyncManager.setAsyncWebRequest(asyncWebRequest); asyncManager.registerCallableInterceptors(this.callableInterceptors); asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); if (asyncManager.hasConcurrentResult()) { Object result = asyncManager.getConcurrentResult(); mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; asyncManager.clearConcurrentResult(); LogFormatUtils.traceDebug(logger, traceOn -> { String formatted = LogFormatUtils.formatValue(result, !traceOn); return "Resume with async result [" + formatted + "]"; }); invocableMethod = invocableMethod.wrapConcurrentResult(result); } invocableMethod.invokeAndHandle(webRequest, mavContainer); // 요청에 대해 동작을 하며, ArgumentResolver동작 if (asyncManager.isConcurrentHandlingStarted()) { return null; } return getModelAndView(mavContainer, modelFactory, webRequest); } finally { webRequest.requestCompleted(); } }
RequestMappingHandlerAdapter -> ServletInvocableHandlerMethod -> InvocableHandlerMethod
InvocableHandlerMethod는 ArgumentResovler를 실행하는 주체입니다.
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } Object[] args = new Object[parameters.length]; for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception ex) { // Leave stack trace for later, exception may actually be resolved and handled... if (logger.isDebugEnabled()) { String exMsg = ex.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw ex; } } return args; }
- resolvers.supportsParameter(): 요청에 ArgumentResolver 적용 조건을 확인합니다.
- 통과한다면, resovlerArgument()를 동작합니다. 그리고 데이터를 가공하여 넘겨줍니다.
ArgumentResovler의 동작 과정을 알아봤으니 CustomArgumentResolver를 만들어보겠습니다.
HandlerMethodArgumentResolver
public interface HandlerMethodArgumentResolver { boolean supportsParameter(MethodParameter parameter); @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }
- ArgumentResolver를 사용하기 원한다면 HandlerMethodArgumentResolver를 상속받고 구현하면 됩니다.
- supportsParameter(): resolveArgument를 실행할지 안 할지 결정하는 역할을 하는 메서드입니다. 판별은 parameter의 어노테이션의 유무로 판별합니다.
- resolveArgument(): supportsParameter값이 true 넘어올 경우 동작합니다. 데이터를 가공하고 return 하면 됩니다.
예시를 보겠습니다.
로그인을 해서 session이 있는 유저에 한하여 값이 넣어질 객체가 SessionDto이며, @LoginUser의 어노테이션이 있다면 session에 저장된 값을 넣어주는 역할을 합니다.
public class ArgumentResolver implements HandlerMethodArgumentResolver{ @Override public boolean supportsParameter(MethodParameter parameter) { boolean hasLoginAnnotation = parameter.hasParameterAnnotation(LoginUser.class); // 어노테이션 유무 판별 boolean hasOwnerType = SessionDto.class.isAssignableFrom(parameter.getParameterType()); // 값을 넣을 객체가 SessionDto인지 확인 return hasLoginAnnotation && hasOwnerType; } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = (HttpServletRequest) webRequest.getNativeRequest(); // 요청을 HttpServletRequest로 바꿔 사용가능하게함 HttpSession session = request.getSession(false); // Session이 없다면 생성하지 않습니다. if(session==null){ // Session이 없다면 Resolver 동작 x return null; } return session.getAttribute(SessionConst.LOGIN_MEMBER); // Session이 있으니 Session에 저장된 값을 SessionDto에 넣어주는 동작을 합니다. } }
정리하며
프로젝트를 만들어보지 않았다면, 굳이 ArgumentResolver를 사용해야 하나 싶을 수 있습니다. 하지만 ArgumentResovler를 사용하지 않는다면, Controller단에서 데이터를 받고 사용할 수 있도록 데이터를 가공하는 코드가 중복으로 들어가게 됩니다. ArgumentResovler를 사용함으로써 Controller단에서 중복된 코드가 없어지며, 값들을 손쉽게 처리할 수 있습니다. 이를 잘 활용하는 것이 코드 가독성과 유지보수에 기여할 수 있기 때문에 사용하는 것을 추천드립니다.
'SpringBoot > spring mvc' 카테고리의 다른 글
Springboot MVC 파헤치기(10) Validation 유효성 검증 (0) | 2022.04.20 |
---|---|
Springboot MVC 파헤치기(9) ViewResolver, HttpMessageConverters (0) | 2022.03.16 |
Springboot MVC 파헤치기(5) @RequestParam (0) | 2022.03.15 |
Springboot MVC 파헤치기(7) @RequestBody, @ResponseBody (0) | 2022.03.15 |
Springboot MVC 파헤치기(6) @ModelAttribute (0) | 2022.03.14 |
댓글
이 글 공유하기
다른 글
-
Springboot MVC 파헤치기(10) Validation 유효성 검증
Springboot MVC 파헤치기(10) Validation 유효성 검증
2022.04.20 -
Springboot MVC 파헤치기(9) ViewResolver, HttpMessageConverters
Springboot MVC 파헤치기(9) ViewResolver, HttpMessageConverters
2022.03.16 -
Springboot MVC 파헤치기(5) @RequestParam
Springboot MVC 파헤치기(5) @RequestParam
2022.03.15 -
Springboot MVC 파헤치기(7) @RequestBody, @ResponseBody
Springboot MVC 파헤치기(7) @RequestBody, @ResponseBody
2022.03.15
댓글을 사용할 수 없습니다.