Backend/Spring

@RequestBody, @ModelAttribute 그리고 Argument Resolver

hou27 2022. 5. 20. 04:42

Spring을 공부하던 중

@ModelAttribute

@RequestBody

이 둘의 차이를 명확히 정리하지 못해 이렇게 해보고 저렇게 해보는 삽질 끝에

결국 깨닫게 되어 정리해두고자 한다.

 

기존 form data를 넘겨받아 유저의 회원가입을 진행하는 필자의 controller는 아래와 같았다.

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
  private final UserService userService;

  @PostMapping("/signUp")
  public User signUp(@ModelAttribute @Validated UserSignUpRequest signUpReq) throws Exception {
    return userService.signUp(signUpReq);
  }
}

보시다시피 생각없이 @ModelAttribute를 사용하여 기능 구현을 완료하고

저 녀석이 뭐하는 친구인지 궁금해져서 한번 없애고 실행해보았다.

 

# 코드 변경 전

System.out.println("signUpReq = " + signUpReq.toString());

Service 중간에 위 코드를 추가하여 객체를 확인하고자 하였다.

 

테스트 결과 :

signUpReq = UserSignUpRequest(email=test@email.com, password=12345, name=김정호)

 

# 코드 변경 후 (@ModelAttribute를 제거)

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserController {
  private final UserService userService;

  @PostMapping("/signUp")
  public User signUp(@Validated UserSignUpRequest signUpReq) throws Exception {
    return userService.signUp(signUpReq);
  }
}

 

 

테스트 결과 :

signUpReq = UserSignUpRequest(email=test@email.com, password=12345, name=김정호)

 

근데 있는 것과 없는 것의 차이가 느껴지지 않았다.

 

그래서 구글링을 통해 알아보았다.

 

@RequestBody

 Client가 body에 데이터(application/json 형태 등의 JSON 및 XML)를 전송하면, body의 내용을 다시 Java Object로 변환해주는 역할을 수행한다.

때문에 body가 있는 HTTP 요청에서만 사용이 가능하다.

 

추가로 body에 담긴 내용을 변환하기 때문에 생성자 및 Setter가 필요하지 않다.

(기본 생성자는 有)

 

@ModelAttribute

 Client가 전송하는 HTTP Parameter 또는 HTTP Body 내용을 Setter 함수를 통해 1:1로 객체에 데이터를 바인딩한다.

중요한 점은 여기서의 HTTP Body 내용은 multipart/form-data 형태라는 것이다.

 

이 어노테이션은 RequestBody 어노테이션과 달리 Setter 또는 기본 생성자가 아닌 필드에 값을 바인딩하는 생성자가 반드시 필요하다.


 

여기까지 알아본 후 두 어노테이션의 차이는 알 수 있었지만,

왜 생략했을 때 문제가 발생하지 않는지를 도저히 알 수 없었다.

 

그러던 중

https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-modelattrib-method-args

 

Web on Reactive Stack

The original web framework included in the Spring Framework, Spring Web MVC, was purpose-built for the Servlet API and Servlet containers. The reactive-stack web framework, Spring WebFlux, was added later in version 5.0. It is fully non-blocking, supports

docs.spring.io

 

Documents를 살펴보던 중

@ModelAttribute For access to an existing attribute in the model (instantiated if not present) with data binding and validation applied. See @ModelAttribute as well as Model and DataBinder.
Note that use of @ModelAttribute is optional — for example, to set its attributes. See “Any other argument” later in this table.

@ModelAttribute의 사용은 선택 사항이라고 하는 것을 확인할 수 있었다.

 

그렇다면 없어도 동작하는 이유가 있을 것 아닌가?

Note that using @ModelAttribute is optional (for example, to set its attributes). By default, any argument that is not a simple value type (as determined by BeanUtils#isSimpleProperty) and is not resolved by any other argument resolver is treated as if it were annotated with @ModelAttribute.

 

위와 같은 설명도 있었는데, 이를 통해 argument resolver 를 검색 키워드로 설정한 후에야

 

Spring에는 다양한 데이터를 처리하여 컨트롤러가 필요로 하는 데이터로 변환해주는

ArgumentResolver

라는 친구가 있다는 것을 알게 되었다.

 

Spring MVC는 사용자가 요청을 보내면 

DispatcherServlet이 요청을 받고, HandlerMapping을 통해 적절한 핸들러를 찾는데,

이 때 RequestMapping으로 구현한 API를 찾는다고 한다.

그 정보를 모두 가지고 있는 RequestMappingHandlerAdapter를 통해 핸들러를 호출하는데,

이 때 Argument Resolver가 동작하여 적절한 데이터의 처리가 이뤄지는 것이라고 한다.

 

그래서 @ModelAttribute를 사용하지 않아도 적절한 Java 객체로 form data가 바인딩되는 것이었다.

 

public @interface RequestMapping

위 Annotation을 살펴보면

Spring MVC와 Spring WebFlux는 각각의 모듈 및 패키지 구조에서 RequestMappingHandlerMapping 및 RequestMappingHandlerAdapter를 통해 이 annotation을 지원한다는 것을 확인할 수 있다.

 

 

사실 이유를 알게 되었을 뿐 정확한 동작원리는 아직 이해하지 못했지만

잠은 잘 수 있을 것 같다.

 

+ 만약 이유를 잘못 이해한 부분이 있다면 알려주신다면 감사하겠습니다.

 


참고자료

 

https://ttl-blog.tistory.com/262

https://rerewww.github.io/spring/spring-handlerAdapter/

https://blog.neonkid.xyz/238

https://velog.io/@hsw0194/Spring-MVC-HandlerMapping

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#webmvc-fn