본문 바로가기
Web/Spring

[Spring] @ModelAttribute

by 희조당 2023. 4. 19.
728x90

🌞 들어가기 앞서

스프링에서 요청한 파라미터를 바인딩하는 방법은 여러 가지 있다.

오늘은 그중에서 @ModelAttribute에 대해서 이야기해 볼 생각이다.

사용은 쉽지만 주의할 점이 있기 때문에 어떻게 동작하는지와 같이 주의점도 정리해 보겠다. 😋😋


🪄 @ModelAttribute란? 

파라미터를 바인딩하는 여러 어노테이션 중에 모델 객체를 바인딩하는 어노테이션이다.

다음과 같은 모델 객체(혹은 DTO)가 존재할 때 정보를 받아오는 방법은 여러 가지가 있겠지만

이 글에서는 @RequestParam과 @ModelAttribute를 비교해서 사용법을 알아보겠다! 가볍게 참고만 하자 ㅎㅎ..

// 모델 객체
public class User {
  private String name;
  private int age;
}

1️⃣ @RequestParam

요청의 쿼리 파라미터나 폼 데이터에서 데이터를 추출해 해당 값을 바로 메소드 파라미터에 할당한다.

@RestController
@RequestMapping("/users")
public class UserController {
  @GetMapping
  @ResponseStatus(OK)
  public void doSomething(@RequestParam("name") String name,
  			@RequestParam("age") int age) {
    User user = new User(name, age);
    userService.doSomething(user);
  }
}

2️⃣ @ModelAttribute

쿼리 파라미터나 폼 데이터에서 여러 개의 파라미터를 추출해서 이를 단일 모델 객체로 묶어 전달한다.

@RestController
@RequestMapping("/users")
public class UserController {
  @GetMapping
  @ResponseStatus(OK)
  public void doSomething(@ModelAttribute User user) {
    userService.doSomething(user);
  }
}

 

두 방식 모두 URI 자체는 다음과 같이 똑같다.

GET https://api.example.com/users?name=name&age=13

🤔 동작원리와 주의점

@ModelAttribute는 생략이 가능한데, 어떻게 동작하는지 살펴보면서 왜 생략해도 무방한지 살펴보자.

🔑 ModelAttributeMethodProcessor

객체를 파라미터로 받고, 이를 처리하는 어떤 어노테이션도 없다면

Spring MVC에서 우선적으로 ModelAttributeMethodProcessor를 통해서 바인딩할 수 있는지 체크한다.

HandlerMethodArgumentResolver의 구현체

이 클래스의 메서드인 supportsParameter()가 요청 매개변수가 바인딩이 가능한지 확인하고,

createAttribute() 메서드를 통해서 바인딩할 객체를 생성한다.

 

이 때, createAttribute() 메서드의 내부 구현을 보면 getResolvableConstructor() 메서드를 사용한다.

이 메서드의 내부 구현을 들여다보면 2가지를 확인할 수 있다.

  • @Primary이 적용된 생성자를 우선적으로 선택한다. 어노테이션이 적용된 생성자가 없다면 가장 많은 파라미터를 받는 생성자를 리턴한다.
  • Java 리플렉션 API를 사용해서 생성자를 가져온다. 

마지막 단계로, resolveArgument()를 호출해서 생성된 객체에 파라미터를 바인딩해 준다.

이 외에 바인딩되지 않은 값은 setter를 통해서 바인딩해 준다.

resolveArgument() 내부

@ModelAttribute에 대해서 어느 정도 공부하신 분이라면 setter가 꼭 필요하다고 생각할 수 있지만 그렇지 않다.

createAttribute()가 바인딩될 파라미터를 가지고 있는 생성자를 선택했다면 setter가 없어도 만들어질 수 있다.

😋 정리

  • 컨트롤러의 파라미터가 객체이고 적용된 어떤 Argument Resolver가 없다면 Spring MVCModelAttributeMethodProcessor를 우선적으로 호출한다.
  • ModelAttributeMethodProcessorsupportsParameter()가 바인딩이 가능할지 확인한다.
  • 바인딩이 가능하다면 createAttribute()를 호출해서 바인딩할 객체를 생성한다. 내부적으로 리플렉션을 사용하고 기본 생성자뿐이라면 기본 생성자를 사용하고, 아니라면 가장 많은 파라미터를 받은 생성자를 선택한다.
  • 객체를 받은 뒤 resolveArgument()에서 파라미터를 바인딩해 준다. Setter가 꼭 필요하지 않다, 선택한 생성자에 따라 다르다.

🪜 나아가기 (@RequestBody)

@RequestBody, @ModelAttribute 두 어노테이션 모두 클라이언트에서 보낸 데이터를 Java 오브젝트로 변경해 준다.

내부적으로 어떻게 차이가 있는지 궁금해져서 글을 작성하다가 추가적으로 찾아봤다.

🛠️ @RequestBody

일반적으로 @RequestBody가 적용된 파라미터에 대해서는 MappingJackson2 HttpMessageConverter를 사용한다. (헤더의 Content-type에 따라 달라질 수 있다.)

여기서 readJavaType()이라는 메서드를 보면 역직렬화를 사용한다. 즉, 여기서도 리플렉션 API가 사용된다.

 

추가적으로, Jackson 공식문서에 따르면 ObjectMapper가 매핑할 때 내부적으로 getter 혹은 setter를 사용한다.

메서드의 접두사(get, set)를 지우고 첫 문자를 소문자를 바꿔서 맞는 값을 찾아간다.

😋 정리

  • @RequestBody, @ModelAttribute 모두 리플렉션 API를 사용한다. 
  • Jackson은 자바 빈즈를 따르기 때문에 알맞는 생성자가 있거나 Getter 혹은 Setter가 구현되어 있어야 한다. (23.11.13 updated)

😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋 

댓글