🌞 들어가기 앞서
스프링에서 요청한 파라미터를 바인딩하는 방법은 여러 가지 있다.
오늘은 그중에서 @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를 통해서 바인딩할 수 있는지 체크한다.
이 클래스의 메서드인 supportsParameter()
가 요청 매개변수가 바인딩이 가능한지 확인하고,
createAttribute()
메서드를 통해서 바인딩할 객체를 생성한다.
이 때, createAttribute()
메서드의 내부 구현을 보면 getResolvableConstructor()
메서드를 사용한다.
이 메서드의 내부 구현을 들여다보면 2가지를 확인할 수 있다.
@Primary
이 적용된 생성자를 우선적으로 선택한다. 어노테이션이 적용된 생성자가 없다면 가장 많은 파라미터를 받는 생성자를 리턴한다.- Java 리플렉션 API를 사용해서 생성자를 가져온다.
마지막 단계로, resolveArgument()
를 호출해서 생성된 객체에 파라미터를 바인딩해 준다.
이 외에 바인딩되지 않은 값은 setter
를 통해서 바인딩해 준다.
@ModelAttribute
에 대해서 어느 정도 공부하신 분이라면 setter
가 꼭 필요하다고 생각할 수 있지만 그렇지 않다.
createAttribute()
가 바인딩될 파라미터를 가지고 있는 생성자를 선택했다면 setter
가 없어도 만들어질 수 있다.
😋 정리
- 컨트롤러의 파라미터가 객체이고 적용된 어떤
Argument Resolver
가 없다면Spring MVC
는ModelAttributeMethodProcessor
를 우선적으로 호출한다. ModelAttributeMethodProcessor
의supportsParameter()
가 바인딩이 가능할지 확인한다.- 바인딩이 가능하다면
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)
😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋
'Web > Spring' 카테고리의 다른 글
[Spring] 트랜잭션 사용 조심하기 (2) | 2023.05.29 |
---|---|
[Spring] @Valid, @Validated과 Custom Annotation (2) (0) | 2023.03.21 |
[Spring] @Valid, @Validated과 Custom Annotation (1) (0) | 2023.03.19 |
[토비의 스프링] 테스트 (2장) (0) | 2023.01.16 |
[토비의 스프링] 오브젝트와 의존관계 (1장) (0) | 2022.12.21 |
댓글