클라이언트가 보내는 HTTP 파라미터들을 Java 객체에 바인딩하는데 사용합니다.
/me?name=woong&age=26 같은 QueryString 형태 혹은 요청 본문에 삽입되어 있는 Form 형태의 데이터를 처리합니다.
DispatcherServlet : 모든 HTTP 요청은 DispatcherServlet에 의해 수신됩니다. 이 서블릿은 프론트 컨트롤러로서 요청을 적절한 핸들러(Controller)로 라우팅합니다.
HandlerMapping : DispatcherServlet은 요청 URL을 기반으로 적절한 핸들러를 찾기 위해 여러 HandlerMapping 구현체를 확인합니다. 예를 들어, RequestMappingHandlerMapping은 @RequestMapping
어노테이션이 붙은 메소드를 찾아서 매핑합니다.
HandlerAdapter : 매핑된 핸들러가 찾아지면, DispatcherServlet은 해당 핸들러를 실행할 수 있는 적절한 HandlerAdapter를 선택합니다. Spring MVC에서는 일반적으로 RequestMappingHandlerAdapter가 사용됩니다.
RequestMappingHandlerAdapter : 이 어댑터는 핸들러 메소드를 호출하기 전에 여러 준비 작업을 수행합니다. 여기에는 요청 파라미터를 메소드의 파라미터에 바인딩하는 작업이 포함됩니다.
HandlerMethodArgumentResolver : RequestMappingHandlerAdapter는 요청 파라미터를 메소드의 각 파라미터에 바인딩하기 위해 여러 HandlerMethodArgumentResolver를 사용합니다. 이 중 하나가 ModelAttributeMethodProcessor입니다.
디버깅 결과 ServletModelAttributeMethodProcessor가 ArgumentResolver로 선택된 것을 확인할 수 있습니다.
ServletModelAttributeMethodProcessor는 ModelAttributeMethodProcessor를 상속받고 있습니다. 결국,
@ModelAttribute
에 대한 정체를 담당하는 ArgumentResolver는 ModelAttributeMethodProcessor임을 확인할 수 있습니다.
supportsParameter
메소드는 주어진 파라미터가 해당 HandlerMethodArgumentResolver
에 의해 처리될 수 있는지 여부를 결정합니다.
MethodAttributeMethodProcessor
의 supportsParameter
는 해당 파라미터에 @ModelAttribute 어노테이션이 붙어있느지 여부와 주어진 타입의 단순 속성 여부로 판단합니다.
supportsParameter
의 결과가 True인 경우 해당 ArgumentResolver가 선택됩니다.
resolveArgument
메소드에서 중요한 두 부분은 constructAttribute
와 bindRequestParameters
입니다. 이 메소드들은 각각 객체를 생성하고, HTTP 요청 파라미터를 해당 객체에 바인딩하는 역할을 합니다.
이를 통해, Spring MVC는 요청 데이터를 적절한 객체로 변환하고 초기화할 수 있습니다.
constructAttribute
메소드를 따라가다보면 BeanUtils.getResolvableConstructor
메소드가 있습니다.
BeanUtils.getResolvableConstructor
는 객체(DTO)의 생성자를 찾아 반환해주는 역할을 합니다. 해당 메소드에 대한 자세한 내용은 좀 더 아래에서 설명하도록 하겠습니다.
객체의 생성자의 파리미터 개수가 0인 경우(기본 생성자)에는 BeanUtils.instantiateClass(ctor)
메소드를 호출합니다.
그 외의 경우(전체 생성자, 부분 생성자 등)에는 BeanUtils.instantiateClass(ctor, args)
메소드를 호출합니다.
BeanUtils.instantiateClass
를 따라가다보면 ctor.newInstance
메소드를 실행하고 있습니다.
ConstructorAccessor.newInstance(args)
메소드는 Java의 Reflection을 사용하여 주어진 인자들을 이용해 특정 클래스의 인스턴스를 생성하는 역할을 합니다.
이제 예시를 통해, 아래 코드와 함께 전체적인 흐름을 분석해보겠습니다.
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)
이 실행되면서 아무 값도 바인딩되지 않은 빈 객체가 만들어집니다.
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)
이 실행되면서 아무 값도 바인딩되지 않은 빈 객체가 만들어집니다.
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
이 발생합니다.
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, value)이 반환됩니다.
createObject - BeanUtils.instantiate(ctor, args)
가 실행되면서 객체에 값이 바인딩됩니다.
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
메소드를 따라가다보면 AbstractPropertyAccessor.setPropertyValues()
메소드가 있습니다.
setPropertyValue(pv)
를 따라가다보면 AbstractNestablePropertyAccessor.processLocalProperty()
라는 메소드가 있습니다.
PropertyHandler
는 속성에 대한 접근과 설정을 관리합니다. ph.isWritable
은 Setter 메소드의 유무를 나타냅니다. 즉, Setter 메소드가 없으면 메소드를 종료하고, 있으면 더 아래의 내용으로 진행합니다.
ph.setValue(valueToApply)
메소드는 값을 필드에 바인딩해주는 역할을 합니다.
Java의 Reflection을 이용해 필드에 값을 바인딩해줍니다.
이러한 과정을 거쳐, DTO에 Setter가 있으면 값을 정상적으로 바인딩해줍니다.
HandlerMethodArgumentResolver
에서 적합한 ArgumentResolver를 찾는 메소드입니다. 선택 가능한 모든 ArgumentResolver 들의 목록을 보면 2개의 ServletModelAttributeMethodProcessor가 존재합니다. 각각 annotationNotRequired
가 True, False 입니다.
@PostMapping("/add")
public String todoAdd(@ModelAttribute TodoDto todoDto) {
log.info("todoDto: {}", todoDto);
return "redirect:/todo/list";
}
/add에 대해 요청이 들어오면 먼저 ServletModelAttributeMethodProcessor(annotationNotRequired=false)
의 supportsParameter
가 실행됩니다.
해당 엔드포인트에는 @ModelAttribute
가 존재하므로 true를 반환해 ArgumentResolver = ServletModelAttributeMethodProcessor가 됩니다.
@PostMapping("/add")
public String todoAdd(TodoDto todoDto) {
log.info("todoDto: {}", todoDto);
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
가 선택됩니다.
저는 개인적으로 Setter는 지양하는 편이어서 가급적 전체 생성자를 사용할 것 같습니다.
잘 읽었습니다.