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