freeParksey
밥세공기
freeParksey
전체 방문자
오늘
어제
  • 분류 전체보기 (150)
    • JAVA (32)
      • 자바스터디 (21)
      • Java in action (6)
      • OOP (1)
      • test (2)
    • 알고리즘 문제 (51)
      • 백준 (49)
    • C (Atmega128) (7)
    • 인공지능 (11)
    • 운영체제 (8)
    • 디자인패턴 (5)
    • 잡다한것 (2)
    • 사용기 (3)
      • 도커 (3)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 3주차
  • Iterator
  • 재귀기초
  • 자바스터디
  • 알고리즘
  • dto 변환 위치
  • 백트랙킹
  • 분류
  • 후기
  • dto 변환
  • 동작 파라미터화
  • 스트림
  • 우테코
  • Thread 동작
  • 동적계획법
  • Python
  • java
  • 자바
  • 우아한테크코스
  • 집합과 맵
  • 그리드
  • 상속
  • generic
  • 백준
  • 프리코스
  • Collection
  • 백트래킹
  • 딥러닝
  • 운영체제
  • Thread #JVM #자바스터디 #

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
freeParksey

밥세공기

카테고리 없음

@ModelAttribute 넌 뭐야?

2023. 7. 13. 15:24

@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;
    }
}

 

먼저 순서는

  1. nestedParameter는 파라미터에 대한 정보를 가져오고 
  2. `getNestedParameterType`을 통해 해당 클래스를 가져오게 됩니다
  3. 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일때 부터 보면되고, 여러 규칙을 찾을 수가 있는데

  1. 리플렉션을 사용하여 클래스의 생성자들에 대한 정보를 가져옵니다. 
  2. public 생성자 부터 확인: 1개이면 사용
  3. public 생성자가 없다면 모든 접근제어자에 대한 생성자 확인
  4. 모든 접근 제어자가 1개이면 사용
  5. 나머지는 기본 생성자를 사용하는데, 없다면 에러 반환

 

`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에 대한 메서드만 적용이 가능합니다.

 

 

 

    freeParksey
    freeParksey
    Github: https://github.com/parksey

    티스토리툴바