@ModelAttribute란?
클라이언트가 보내는 HTTP 파라미터들을 Java 객체에 바인딩하는데 사용합니다.
/me?name=woong&age=26 같은 QueryString 형태 혹은 요청 본문에 삽입되어 있는 Form 형태의 데이터를 처리합니다.
주요 Flow
1. 요청 수신
DispatcherServlet : 모든 HTTP 요청은 DispatcherServlet에 의해 수신됩니다. 이 서블릿은 프론트 컨트롤러로서 요청을 적절한 핸들러(Controller)로 라우팅합니다.
2. 핸들러 매핑
HandlerMapping : DispatcherServlet은 요청 URL을 기반으로 적절한 핸들러를 찾기 위해 여러 HandlerMapping 구현체를 확인합니다. 예를 들어, RequestMappingHandlerMapping은 @RequestMapping 어노테이션이 붙은 메소드를 찾아서 매핑합니다.
3. 핸들러 어댑터 선택
HandlerAdapter : 매핑된 핸들러가 찾아지면, DispatcherServlet은 해당 핸들러를 실행할 수 있는 적절한 HandlerAdapter를 선택합니다. Spring MVC에서는 일반적으로 RequestMappingHandlerAdapter가 사용됩니다.
4. 요청 파라미터 바인딩 준비
RequestMappingHandlerAdapter : 이 어댑터는 핸들러 메소드를 호출하기 전에 여러 준비 작업을 수행합니다. 여기에는 요청 파라미터를 메소드의 파라미터에 바인딩하는 작업이 포함됩니다.
5. HandlerMethodArgumentResolver 호출
HandlerMethodArgumentResolver : RequestMappingHandlerAdapter는 요청 파라미터를 메소드의 각 파라미터에 바인딩하기 위해 여러 HandlerMethodArgumentResolver를 사용합니다.
이 중 하나가 ModelAttributeMethodProcessor입니다.
디버깅 결과 ServletModelAttributeMethodProcessor가 ArgumentResolver로 선택된 것을 확인할 수 있다.
ServletModelAttributeMethodProcessor는 ModelAttributeMethodProcessor를 상속받고 있다.
결국, @ModelAttribute에 대한 정제를 담당하는 ArgumentResolver는 ModelAttributeMethodProcessor임을 확인할 수 있다.
ModelAttributeMethodProcessor - supportsParameter
supportsParameter 메소드는 주어진 파라미터가 해당 HandlerMethodArgumentResolver에 의해 처리될 수 있는지 여부를 결정합니다.
ModelAttributeMethodProcessor의 supportsParameter는 해당 파라미터에 @ModelAttribute 어노테이션이 붙어있는지 여부와 주어진 타입의 단순 속성 여부로 판단합니다.
supportsParameter의 결과가 True인 경우 해당 ArgumentResolver가 선택됩니다.
ModelAttributeMethodProcessor - resolveArgument
resolveArgument 메소드에서 중요한 두 부분은 constructAttribute와 bindRequestParameters 입니다.
이 메소드들은 각각 객체를 생성하고, HTTP 요청 파라미터를 해당 객체에 바인딩하는 역할을 합니다.
이를 통해 Spring MVC는 요청 데이터를 적절한 객체로 변환하고 초기화할 수 있습니다.
contructAttribute(binder, webRequest)
contructAttribute 메소드를 따라가다보면 BeanUtils.getResolvableConstructor() 메소드가 있습니다.
BeanUtils.getResolvableConstructor() 는 객체(DTO)의 생성자를 찾아 반환해주는 역할을 합니다.
해당 메소드에 대한 자세한 내용은 좀 더 아래에서 설명하도록 하겠습니다.
객체의 생성자의 파라미터 개수가 0인 경우(기본 생성자)에는 BeanUtils.instantiateClass(ctor) 메소드를 호출합니다.
그 외의 경우(전체 생성자, 부분 생성자 등)에는, BeanUtils.instantiateClass(ctor, args) 메소드를 호출합니다.
BeanUtils.instantiateClass()를 따라가다보면 ctor.newInstance() 메소드를 실행하고 있습니다.
ConstructorAccessor.newInstance(args) 메소드는 Java의 리플렉션을 사용하여 주어진 인자들을 이용해 특정 클래스의 인스턴스를 생성하는 역할을 합니다.
이제 예시를 통해, 아래 코드와 함께 전체적인 흐름을 분석해보겠습니다.
DTO에 기본 생성자만 존재하는 경우
public class TodoDto {
private String title;
private int value;
public TodoDto() {
}
}
// 결과
TodoDto(title=null, value=0)
해당 코드는 Java 언어이므로 1번의 결과는 Null이 나옵니다.
2번의 결과는 [TodoDto()]이므로 TodoDto()를 반환하게 됩니다.
createObject - BeanUtils.instantiateClass(ctor)이 실행되면서 아무 값도 바인딩되지 않은 빈 객체가 만들어집니다.
DTO에 기본 생성자와 전체 생성자가 함께 존재하는 경우
public class TodoDto {
private String title;
private int value;
public TodoDto() {
}
public TodoDto(String title, int value) {
this.title = title;
this.value = value;
}
}
// 결과
TodoDto(title=null, value=0)
마찬가지로, 1번의 결과는 Null입니다(앞으로는 생략하겠습니다..).
2번의 결과는 [TodoDto(), TodoDto(title, value)]이므로 4번 메소드가 실행됩니다.
결국, 기본 생성자가 반환되므로 createObject - BeanUtils.instantiateClass(ctor)이 실행되면서 아무 값도 바인딩되지 않은 빈 객체가 만들어집니다.
DTO에 부분 생성자와 전체 생성자가 함께 존재하는 경우
public class TodoDto {
private String title;
private int value;
public TodoDto(String title) {
this.title = title;
}
public TodoDto(String title, int value) {
this.title = title;
this.value = value;
}
}
No primary or single unique constructor found for class XXX 라는 오류가 발생합니다.
2번의 결과는 [TodoDto(title), TodoDto(title, value)]이므로 4번 메소드가 실행됩니다.
하지만, 기본 생성자가 존재하지 않으므로 throw new IllegalStateException()이 발생합니다.
DTO에 전체 생성자만 존재하는 경우
public class TodoDto {
private String title;
private int value;
public TodoDto(String title, int value) {
this.title = title;
this.value = value;
}
}
// 결과
TodoDto(title=test, value=1)
2번의 결과는 [TodoDto(title, value)]이므로 TodoDto(title)이 반환됩니다.
createObject - BeanUtils.instantiateClass(ctor, args)가 실행되면서 객체에 값이 바인딩됩니다.
DTO에 Private 생성자만 존재하는 경우
public class TodoDto {
private String title;
private int value;
private TodoDto(String title, int value) {
this.title = title;
this.value = value;
}
}
// 결과
TodoDto(title=test, value=1)
2번의 결과는 []이므로, 3번 메소드가 실행됩니다.
접근 제어자와 상관없이 모든 생성자를 반환하므로 TodoDto(title, value)가 반환되면서 객체에 값이 정상적으로 바인딩됩니다.
bindRequestParameters(binder, webRequest)
bindRequestParameters 메소드를 따라가다보면 AbstractPropertyAccessor.setPropertyValues() 메소드가 있습니다.
setPropertyValue(pv)를 따라가다보면 AbstractNestablePropertyAccessor.processLocalProperty()라는 메소드가 있습니다.
PropertyHandler는 속성에 대한 접근과 설정을 관리합니다. ph.isWritable()은 Setter 메소드의 유무를 나타냅니다.
즉, Setter 메소드가 없으면 메소드를 종료하고, 있으면 더 아래의 내용으로 진행합니다.
ph.setValue(valueToApply) 메소드는 값을 필드에 바인딩해주는 역할을 합니다.
Java의 Reflection을 이용해 필드에 값을 바인딩해줍니다.
이러한 과정을 거쳐, DTO에 Setter가 있으면 값을 정상적으로 바인딩해줍니다.
@ModelAttribute가 생략이 가능한 이유
HandlerMethodArgumentResolver에서 적합한 ArgumentResolver를 찾는 메소드입니다. 선택 가능한 모든 ArgumentResolver들의 목록을 보면 2개의 ServletModelAttriburteMethodProcessor가 존재합니다. 각각 annotationNotRequired가 true, false입니다.
@ModelAttribute가 존재하는 경우
@PostMapping("/add")
public String todoAdd(@ModelAttribute TodoAddDto todoAddDto) {
log.info("todoAddDto: {}", todoAddDto);
return "redirect:/todo/list";
}
/add에 대해 요청이 들어오면 먼저 annotationNotRequired가 false인 ServletModelAttributeMethodProcessor의 supportsParameter가 실행됩니다.
해당 엔드포인트에는 @ModelAttribute가 존재하므로 true를 반환해 ArgumentResolver = ServletModelAttributeMethodProcessor가 됩니다.
@ModelAttribute가 존재하지 않는 경우
@PostMapping("/add")
public String todoAdd(TodoAddDto todoAddDto) {
log.info("todoAddDto: {}", todoAddDto);
return "redirect:/todo/list";
}
위와 같이 먼저 ServletModelAttributeMethodProcessor(annotationNotRequired=false)의 supportsParameter가 실행됩니다. 해당 엔드포인트에는 @ModelAttribute가 존재하지 않으므로 parameter.hasParameterAnnotation(ModelAttribute.class)는 false가 반환됩니다. this.annotationNotRequired 또한 false이므로 최종적으로 false를 반환하게 됩니다.
그 다음, ArgumentResolver들을 순회하다가 두 번째 ServletModelAttributeMethodProcessor(annotationNotRequired=true)의 supportsParameter가 실행됩니다. this.annotationNotRequired는 true이며, BeanUtils.isSimpleProperty의 결과가 false이므로 최종적으로 true를 반환하게 됩니다. 결과적으로, ArgumentResolver = ServletModelAttributeMethodProcessor가 됩니다.
결국, @ModelAttribute의 존재 여부와는 상관없이 모두 ServletModelAttributeMethodProcessor가 선택됩니다.
정리
@ModelAttribute를 정상적으로 사용하기 위한 조건
- 전체 생성자
- 기본 생성자 + Setter
- 전체 생성자 + Setter
- 기본 생성자 + 전체 생성자 + Setter
저는 개인적으로 Setter는 지양한다는 편이어서 가급적 전체 생성자를 사용할 것 같습니다.
@ModelAttribute는 생략 가능이 가능하다.
'Programming > SpringBoot' 카테고리의 다른 글
[10k-Chat] Spring Boot 실시간 채팅 서버 구현 (1) - Stomp (0) | 2025.01.19 |
---|---|
[SpringBoot] JPA 동적 스키마 (1) | 2024.11.06 |
[SpringBoot] Entity의 ID를 테스트에서 삽입하는 방법 (0) | 2024.06.02 |
[SpringBoot] @ResponseBody 원리 (0) | 2024.01.29 |
[SpringBoot] Model VS ModelMap (0) | 2023.12.26 |