Spring

@ModelAttribute 어노테이션을 생략했을 때 파라미터가 바인딩되는 과정

지금 느낌 그대로 2023. 8. 25. 00:33

들어가며

Spring MVC 구조에서 컨트롤러 코드를 작성할 때, 클라이언트에서 넘어온 정보를 DTO 클래스를 만들어서 바인딩하는 경우가 많습니다. 이렇게 구현하면 파라미터가 많은 경우 하나의 클래스에 모아서 관리하기 편하고, 비즈니스 로직을 처리할 서비스 계층에서도 다루기 편하기 때문에 많이 사용됩니다.

 

코드로 작성하면 아마 다음과 비슷한 구조겠죠.

@GetMapping("/api/v1/admin/users")
    public ResponseEntity<CommonResponse<List<UserResponse>>> list(UserSearchRequest userSearch, Pageable pageable) {

        Page<User> result = userViewService.findAll(userSearch, pageable);
        List<UserResponse> responseContent = result.getContent().stream().map(User::toResponseDto).toList();

        return ResponseEntity.ok().body(new CommonResponse<>(responseContent, result.getTotalElements()));

    }

클라이언트에서는 /api/v1/admin/users?size=10&name=myuser&state=O 라는 url로 요청을 보내면 UserSearch 객체에  name 필드와 state필드가 존재하고 적절한 타입이라면 개발자가 별도의 코드를 작성하지 않아도 바인딩됩니다.

어떻게 아무런 어노테이션이나 코드를 작성하지 않고도 UserSearch객체에 문자열이 바인딩 되는걸까요?

심지어 UserSearchRequest 클래스 내부의 필드이름으로 요청해도 UserSearch객체의 필드에 매핑이됩니다.

어떻게 해서 클라이언트에서 보낸 요청이 최종적으로 UserSearchRequest 객체에 바인딩되는지 따라가보도록하겠습니다.

 

UserSearch 객체에 바인딩 되는 과정

Spring 에서 Controller의 endpoint 메소드의 파라미터는 아무런 어노테이션을 붙이지 않으면 int, String 같은 단순 자료형은 @RequestParam으로 인식하고 그 외의 객체는 @ModelAttribute가 있는걸로 인식합니다. 그래서 클라이언트의 요청을 DTO로 받을 때, @ModelAttribute 어노테이션을 선언하지 않고도 바인딩할 수 있습니다.

이유는 스프링에서 어노테이션이 없는 파라미터의 바인딩을 처리하는 클래스를 미리 등록해놓았기 때문입니다.

 

스프링은 ArgumentResolver를 통해서 컨트롤러의 인자에 지정된 변수들을 Annotation이나 객체의 타입에 따라서 Resolver를 거쳐 실제 데이터를 Controller에 넘겨줍니다.

ArgumentResolver라는 이름부터가 인자를 해결한다는 의미입니다.

그리고 스프링 서버를 실행하면 많은 일을 하지만 그 중 Spring Bean을 등록하는 과정 중간에 미리 정의된 ArgumentResolver 들을 등록합니다.

이 과정에서 다양한 타입의 argument들을 처리할 수 있는 resolver를 등록하고 클라이언트에서 넘어온 인자들을 적합한 resolver가 처리하는 흐름을 가지고 있습니다. 

 

이제 구체적인 코드를 보면서 과정을 따라가 보겠습니다.

AbstractAutowireCapableBeanFactory&nbsp; - invokeInitMethods

위 사진은 스프링이 Bean을 생성하는 코드를 따라가다 argumentResolver를 등록하는 메소드가 포함된 메소드를 캡쳐한 것입니다.

AbstractAutowireCapableBeanFactory 클래스의 Bean을 생성하는 createBean 메소드를 따라가다 보면 Bean을 생성하고 초기화하는 메소드를 호출하면서 invokeInitMethods 메소드를 호출합니다.

메소드 중간부분에 afterPropertiesSet 메소드에서 ArgumentResolver들을 등록하는 역할을 합니다.

 

afterPropertiesSet 메소드의 내용을 보면 getDefaultArgumentResolvers 메소드에서 default argumentResolver들을 얻어옵니다. default argumentResolver는 위에서 이야기한 스프링이 다양한 타입의 파라미터들을 바인딩하기 위해 미리 정의한 ArgumentResolver를 의미합니다. 

 

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {

	// Annotation-based argument resolution
    // 생략
    
    // Type-based argument resolution
    // 생략
    
    // Catch-all
    resolvers.add(new PrincipalMethodArgumentResolver());
    resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
    resolvers.add(new ServletModelAttributeMethodProcessor(true));

    return resolvers;
}

 

코드가 길어서 위에부분은 생략하고 우리가 관심있는 주제인 UserSearchRequest 객체에 아무런 어노테이션이 없지만 정상적으로 바인딩되는 원인을 알아보기 위해서 어노테이션기반이나 타입기반의 resolver가 아닌 경우를 처리하는 Catach-all 부분을 살펴보겠습니다. 

UserSearchRequest 는 아무런 어노테이션이 없기에 어노테이션 기반에 포함되지 않고, type 기반은 ServletRequest, MultipartRequest, SessionStatus와 같은 클래스에 등록되어 있는 타입들을 처리하기 때문에 포함되지 않습니다.

그렇게 자연스럽게 다음으로 넘어가서 Catch-all 주석 아래부분의 resolver들을 살펴보겠습니다.

 

이 중 ServletModelAttributeMethodProcessor를 살펴보겠습니다.

여기서 생성자의 인자로 true를 전달하는데, 잘 기억해두고 다음으로 넘어가서 이야기해보도록 하겠습니다.

 

 

 ServletModelAttributeMethodProcessor 클래스 코드를 보면 이 클래스가 ModelAttributeMethodProcessor를 상속한 클래스라는 것을 알 수 있습니다.

ServletModelAttributeMethodProcessor 클래스 코드에는 이 클래스가 어떤 파라미터들을 처리하는 지 나타나있지 않기때문에 부모클래스인 ModelAttributeMethodProcessor를 살펴보겠습니다.

 

ServletModelAttributeMethodProcessor 클래스는 HandlerArgumentResolver 클래스를 구현한 구현클래스로 HandlerArgumentResolver 가 지원하는 범위는 supprotParameter 메소드를 보면 알수 있습니다. supportParameter 메소드는 이 클래스가 어떤 파라미터를 처리할 지 나타내는 메소드입니다.

 

supportParameter를 보면 @ModelAttribute 어노테이션을 가지고 있거나 annotationNotRequired 필드값이 true면서 파라미터 타입이 SimpleProperty가 아닌경우를 지원합니다.

SimpleProperty가 아니라는 것은 Int, String과 같은 자바 기본타입과 이를 감싸는 Wrapper 타입, Enum과 Date, CharSuquence, Number, Temporal, URI, URL, Locale, Class를 제외한 타입을 말합니다.

아까 기억을 떠올려서 ServletModelAttributeMethodProcessor의 생성자의 인자로 true를 전달했고, ServletModelAttributeMethodProcessor클래스는 부모클래스의 annotationNotRequired 을 사용하므로, annotationNotRequired 값은 true가 됩니다.

이 말은 어노테이션이 없는 경우를 처리할 수 있다는 의미입니다.

 

이제 다시 돌아가서, 클라이언트에서 요청이 왔을 때 스프링 흐름을 정리해보겠습니다.

먼저 클라이언트에서 보낸 요청이 DispatcherServlet에 전달되고 요청을 처리할 수 있는 HandlerAdapter를 찾은뒤에 handle() 메소드를 실행합니다. 

 

출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1 교재 자료

그리고 이 과정에서 등록된 ArgumentResolver들을 순회하면서 클라이언트에서 넘어온 argument를 처리할 수 있는 ArgumentResolver를 찾습니다. 이 글의 초반부에 이야기한 파라미터(어노테이션이 없고 기본타입이 아닌 파라미터)는 

ServletModelAttributeMethodProcessor에서 처리할 수 있으므로 ServletModelAttributeMethodProcessor가 이 Argument를 처리할 resolver로 선택됩니다.

그리고 ArgumentResolver에서 argument를 분석해서 파라미터에 바인딩 시켜줍니다.

 

마무리

여기까지해서 컨트롤러에서 선언한 아무런 어노테이션이 없는 DTO 클래스인 UserSearchRequest 객체에 바인딩되는 과정을 살펴보았습니다. 이 글에서는 전체적인 과정을 이야기하는 것에 초점을 두었으므로 바인딩하는 구체적인 코드까지 살펴보지는 않았습니다. Argument를 분석해서 바인딩하는 구체적인 내용은 ModelAttributeMethodProcessor 클래스와 ServletModelAttributeMethodProcessor 클래스의 resolveArgument 메소드를 보면 자세히 나와있습니다.