@ModelAttribute란?
spring 프레임워크에서 제공해주는 어노테이션, 웹에서 요청 파라미터에 따라 값을 전달 받아 객체로 사용하고 싶을 때 바인딩 해준다
@ModelAttribute를 사용하는 방식에 `method`와 `parameter`방식이 있습니다.
[ Method 방식 ]
@ModelAttribute
public void addAttributes(Model model) {
model.addAttribute("status", "Customer");
}
하나의 전역 값 추가라고 생각할 수 있습니다. 따라서 모든 controller에서 model에 접근하게 되면 status를 접근할 수 있게 되는 것입니다.
하나의 특징은 @RequestMapping이 실행되기 전에 실행되는데, 전역으로 Model객체에 추가해 주는 만큼 Model 객체를 사전에 생성해 줘야하기 때문입니다.
[ Parameter 방식 ]
@PostMapping("/")
public String findCustomer(@ModelAttribute("data") CustomerUpdateRequest updateRequest, Model model) {
// 내용
}
뷰에서 "data"를 통해 파라미터를 받아오게 되면 바인딩이 되는데 springframework에서 자동으로 해줄 뿐 자세한 내용은 잘 모르게 된다. 따라서 이에 대해 발을 살짝 담가보면서 어떻게 동작되며, 조건이 무엇인지 확인해 보겠습니다!
어떻게 동작할까?
[ 바인딩 처리 전까지 여정 ]
먼저, 클라이언트의 요청을 가장 먼저 처리하는 곳은 DispatcherServlet 클래스입니다.
과정 생략 가능
public class DispatcherServlet extends FrameworkServlet {
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 내용
this.doDispatch(request, response);
}
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 내용
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
}
}
1. doService 실행 -> doDispatch
2. handle() : 뷰를 통해 입력받은 파라미터 데이터들을 처리하기 위해 HandlerAdapter 사용
public abstract class AbstractHandlerMethodAdapter
extends WebContentGenerator
implements HandlerAdapter, Ordered {
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return this.handleInternal(request, response, (HandlerMethod)handler);
}
@Nullable
protected abstract ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception;
}
3. AbstractHandlerMethodAdapter를 상속받는 클래스에서 handle을 넘겨준다.
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// 내용
mav = this.invokeHandlerMethod(request, response, handlerMethod);
}
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
// 내용
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
}
}
4. RequestMappingHandlerAdapter : AbstractHandlerMethodAdapter 상속
5. 요청에 대한 매핑을 하기 위해 handlerMethod 사용
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);
// 내용
}
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
//
}
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
for(int i = 0; i < parameters.length; ++i) {
//내용
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
}
6. ServletInvocableHandlerMethod의 invokeAndHandle() 호출
7. 내부 메서드 invokeForRequest() 호출 -> 여기서 for문은 각각의 파라미터들을 하나씩 가져온다.
Ex) RequestDto, Model이 있다고 하면 각 for문은 RequestDto, Model을 순서대로 처리하는 것
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 내용
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
}
8. ModelAttributeMethodProcessor를 통해 데이터 처리하기 위해 resolveArgument호출하여
최종적으로 수행하는 클래스까지 올 수 있습니다.
[ 처리하는 클래스 ]
아래 ModelAttributeMethodProcessor가 이제 파라미터를 바인딩해 주는 클래스입니다.
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
attribute = this.createAttribute(name, parameter, binderFactory, webRequest);
}
protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
MethodParameter nestedParameter = parameter.nestedIfOptional();
Class<?> clazz = nestedParameter.getNestedParameterType();
Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);
Object attribute = this.constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);
if (parameter != nestedParameter) {
attribute = Optional.of(attribute);
}
return attribute;
}
}
먼저 순서는
- nestedParameter는 파라미터에 대한 정보를 가져오고
- `getNestedParameterType`을 통해 해당 클래스를 가져오게 됩니다
- BeanUtils.getResovableConstructor에서 클래스에 대한 생성자를 가져오게 됩니다.
문제는 생성자를 가져오는 부분인데
// BeanUtils
public static <T> Constructor<T> getResolvableConstructor(Class<T> clazz) {
Constructor<T> ctor = findPrimaryConstructor(clazz);
if (ctor != null) {
return ctor;
} else {
Constructor<?>[] ctors = clazz.getConstructors();
if (ctors.length == 1) {
return ctors[0];
} else {
if (ctors.length == 0) {
ctors = clazz.getDeclaredConstructors();
if (ctors.length == 1) {
return ctors[0];
}
}
try {
return clazz.getDeclaredConstructor();
} catch (NoSuchMethodException var4) {
throw new IllegalStateException("No primary or single unique constructor found for " + clazz);
}
}
}
}
클래스를 가져올 때
처음에는 `findPrimaryConstructor`에서 생성자를 가져오는데 여기는 Kotlin에 관한 클래스 였던 거 같아서 넘기겠습니다.
이후 ctor==null일때 부터 보면되고, 여러 규칙을 찾을 수가 있는데
- 리플렉션을 사용하여 클래스의 생성자들에 대한 정보를 가져옵니다.
- public 생성자 부터 확인: 1개이면 사용
- public 생성자가 없다면 모든 접근제어자에 대한 생성자 확인
- 모든 접근 제어자가 1개이면 사용
- 나머지는 기본 생성자를 사용하는데, 없다면 에러 반환
`MethodParameter`, `Class`, `Constructor`를 통해 각 정보들을 가져와서 constructAttribute메서드에서 먼저 생성자를 통한 파라미터 주입을 시도합니다.
파라미터가 없다면 인스턴스를 바로 반환하고
if (ctor.getParameterCount() == 0) {
return BeanUtils.instantiateClass(ctor, new Object[0]);
}
생성자 파라미터 변수들의 값과 입력 파라미터들의 변수 명을 비교해서 args를 생성해 주게 됩니다.
이때 입력 파라미터와 동일한 이름이 없다고 한다면
- ! + 생성자 파라미터 == 입력 파라미터
- _ + 생성자 파라미터 == 입력 파라미터
를 통해 다시 비교하여 값을 주입할 수 있는지 확인하고 없다면 null을 반환하여 데이터를 반환합니다.
for(int i = 0; i < paramNames.length; ++i) {
String paramName = paramNames[i];
Class<?> paramType = paramTypes[i];
value = webRequest.getParameterValues(paramName);
if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) {
value = Array.get(value, 0);
}
if (value == null) {
if (fieldDefaultPrefix != null) {
value = webRequest.getParameter(fieldDefaultPrefix + paramName);
}
if (value == null) {
if (fieldMarkerPrefix != null && webRequest.getParameter(fieldMarkerPrefix + paramName) != null) {
value = binder.getEmptyValue(paramType);
} else {
value = this.resolveConstructorArgument(paramName, paramType, webRequest);
}
}
}
try {
MethodParameter methodParam = new FieldAwareConstructorParameter(ctor, i, paramName);
if (value == null && methodParam.isOptional()) {
args[i] = methodParam.getParameterType() == Optional.class ? Optional.empty() : null;
} else {
args[i] = binder.convertIfNecessary(value, paramType, methodParam);
}
} catch (TypeMismatchException var21) {
var21.initPropertyName(paramName);
args[i] = null;
failedParams.add(paramName);
binder.getBindingResult().recordFieldValue(paramName, paramType, value);
binder.getBindingErrorProcessor().processPropertyAccessException(var21, binder.getBindingResult());
bindingFailure = true;
}
}
예를들어
입력 파라미터로는 email이 들어오는데 생성자에서 email2를 받고 있다고 가정하자
이러면 그렇게 된다면 !email2와 _eamil2가 email과 같은지를 확인한다는 것이다.
public CustomerCreationRequest(String email2, String name) {
this.email = email2;
this.name = name;
}
이제 이후에는 새로운 바인더를 추가하게 되는데 setter를 통한 바이드를 할 수 있는지를 확인한다.
// resolveArgument메서드 내부, attribute생성 후
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
this.bindRequestParameters(binder, webRequest);
}
this.validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
public class DefaultDataBinderFactory implements WebDataBinderFactory {
public final WebDataBinder createBinder(NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {
WebDataBinder dataBinder = this.createBinderInstance(target, objectName, webRequest);
if (this.initializer != null) {
this.initializer.initBinder(dataBinder);
}
this.initBinder(dataBinder, webRequest);
return dataBinder;
}
}
여기서는 initializer를 통해 데이터를 바인드 할 수 있는지 확인하고 바인드를 할 수 있는 상태로 만들게 된다.
이후 만약 바인드가 되어 있지 않았다면 `bindRequestParameters`를 실행하여 바인드를 해주게 된다.
결론
1. 생성자 생성
- public 생성자 부터 1개인지 확인 -> 1개이면 끝
- public 생성자가 0개이면 -> 모든 접근제어자에 대한 생성자 확인
- 이 또한 생성자가 1개이면 끝
- 나머지는 전부 기본 생성자 생성한다. (파라미터가 없는 기본 생성자)
2. 파라미터 매핑
- 파라미터 명이 동일하거나 생성자 파라미터에 `!`이나 `_`를 붙여서 같은지 확인
- 다르면 null 넣어줌
3. init확인
- 값을 초기화 할 수 있는 메서드가 있는지 확인하여 값을 넣어준다
- 여기서 init메서드의 경우 JavaBeans 규약에 따라 set에 대한 메서드만 적용이 가능합니다.