- ArgumentResolver
- HandlerMethodArgumentResolver
- ModelAttributeMethodProcessor
- supportsParameter
- resolveArgument
- resolveArgument의 세부적인 흐름 파악해보기
- 1. ServletModelAttributeMethodProcess - "createAttribute"
- 2. ModelAttributeMethodProcess - "createAttribute"
- 3. BeanUtils - "getResolvableConstructor"
- 4. ModelAttributeMethodProcess - "constructAttribute"
- 5. BeanUtils - "instantiateClass"
- 6. 결론
- 의문점
- 1. Default Constructor + getter + setter
- 2. AllArgsConstructor + getter + setter
- 3. AllArgsConstructor + getter
- 4. Default Constructor + getter
- 5. Default Constructor + AllArgsConstructor
- @ModelAttribute 처리 흐름 결론
- 1. 정상 바인딩 조합
- 2. null 바인딩 조합
- 3. Resolved ~~ Could not find acceptable representation 예외 발생 조합
ArgumentResolver
Spring Framework에서 ArgumentResolver는 굉장히 중요한 핵심 개념이다
사용자의 Request가 들어오는 순간 처리 메커니즘을 간단하게 알아보자
1. 사용자의 Request가 DispatcherServlet으로 들어온다
2. DispatcherServlet은 들어온 Request를 처리할 수 있는 Handler를 조회
3. 조회한 Handler를 컨트롤할 수 있는 HandlerAdapter 조회
4. HandlerAdapter가 본인이 컨트롤하는 Handler를 invoke
4단계 :: Handler를 invoke하는 프로세스에서 ArgumentResolver가 활용된다
- 이 말이 무슨 의미인지 정확하게 이해가 되지 않을 수 있으니 상세하게 풀어서 살펴보자

현재 RestController인 TestController에 Endpoint GET /hello가 존재한다
위의 과정에 대입해보면 다음과 같다
1. 사용자의 GET /hello가 DispatcherServlet으로 들어온다
2. DispatcherServlet은 들어온 Request를 처리할 수 있는 Handler를 조회
→ @GetMapping("/hello") invokeHello
3. 조회한 Handler를 컨트롤할 수 있는 HandlerAdapter 조회
→ RequestMappingHandlerAdapter
4. HandlerAdapter가 본인이 컨트롤하는 Handler를 invoke
여기서 RequestMappingHandlerAdapter는 invokeHello를 invoke 하기 전에 Handler(invokeHello)가 Hello라는 Object Data를 활용할 수 있도록 @ModelAttribute Hello hello라는 파라미터를 정제하는 과정을 수행한다
HandlerMethodArgumentResolver

이 중에서 과연 위의 invokeHandler의 @ModelAttribute Hello hello를 정제할 수 있는 ArgumentResolver가 무엇인지 확인해보자
HandlerMethodArgumentResolverComposite - "getArgumentResolver"

무슨 ArgumentResolver가 선택될지 디버깅을 해보니 ServletModelAttributeMethodProcessor가 선택이 되었다


클래스 구조를 파악해보니 결국 @ModelAttribute에 대한 정제를 담당하는 ArgumentResolver는 ModelAttributeMethodProcessor임을 확인할 수 있다
- 이 추정에 대한 확신을 가지기 위해서 디버깅을 통해서 실제로 ModelAttributeMethodProcessor로 흐름이 넘어가는지 확인해보자


ModelAttributeMethodProcessor로 넘어옴을 디버깅을 통해서 확인할 수 있다
ModelAttributeMethodProcessor
ModelAttributeMethodProcessor가 @ModelAttribute에 대한 처리 메커니즘을 가진다는 것을 위의 디버깅 과정을 통해서 확인하였다
그러면 이제 ModelAttributeMethodProcessor의 내부 동작 메커니즘을 확인해보자
supportsParameter

일단 @ModelAttribute라는 바인딩 애노테이션이 어떤 파라미터 타입에 적용되는지 살펴보자
- 코드를 보면 !BeanUtils.isSimpleProperty라는 부분을 확인할 수 있다

따라서 @ModelAttribute는 위의 코드에서 볼 수 있는 타입이 아니면 적용될 수 있는 것이다
resolveArgument
어떤 파라미터에 적용 가능한지 확인을 하였으니 이제 resolveArgument의 동작 흐름을 알아보자

resolveArgument에 진입하게 되면 파라미터 이름, ModelAttribute 속성, ...등 여러가지 정보를 얻는다
그리고 가장 핵심적인 createAttribute부분으로 진입하게 된다
- resolveArgument의 최종 응답 값은 attribute이다
- 따라서 우리는 createAttribute를 통해서 Hello hello라는 Object에 RequestData가 Binding됨을 추측할 수 있다
resolveArgument의 세부적인 흐름 파악해보기
1. ServletModelAttributeMethodProcess - "createAttribute"



createAttribute의 getRequestValueForAttribute의 값이 null로 도출됨에 따라 super.createAttribute로 흐름이 넘어가게 된다

- 이 구조를 통해서 super.createAttribute는 결국 ModelAttributeMethodProcessor로 넘어감을 확인할 수 있다
2. ModelAttributeMethodProcess - "createAttribute"

GET /hello : invokeHello의 파라미터인 Hello hello에 대한 생성자를 얻는듯한 로직을 볼 수 있다
@Getter
@AllArgsConstructor
static class Hello {
private Long id;
private String name;
}
현재 Hello는 위와 같이 구성되어 있고 위의 로직을 통해서 어떠한 생성자를 얻는지 확인해보자
- 사실 당연히 @AllArgsConstructor가 존재하므로 public Hello(Long id, String name)을 얻을거라고 예상된다
- 추가적으로 자바 기본 상식이지만 @AllArgsConstructor에 의해 생성자가 이미 존재하기 때문에 컴파일러가 기본 생성자는 만들어주지 않는다
3. BeanUtils - "getResolvableConstructor"

예상한대로 public Hello(Long id, String name) 생성자를 가져옴을 확인할 수 있다
4. ModelAttributeMethodProcess - "constructAttribute"

파라미터 Hello의 선언된 생성자를 얻어내고 constructAttribute흐름으로 넘어가보자
- constructAttribute에서 요청으로 들어온 데이터들의 Binding이 이루어지는 느낌이다

현재 얻은 Hello의 생성자는 Long id, String name 총 2개의 파라미터를 보유하고 있기 때문에 위의 if 로직에는 포함되지 않는다
계속해서 다음 코드들을 살펴보자

- 위의 코드들은 Binding을 위한 기본적인 프로세스를 진행한다고 생각하면 된다

- 현재 요청은 http://localhost:8080/hello?id=1&name=spring형식으로 진행되고 있다

일련의 과정을 통해서 Object[] args에 현재 요청으로 들어온 데이터들을 담았고 이제 BeanUtils.instantiateClass로 흐름을 넘겨보자
5. BeanUtils - "instantiateClass"

첫번째로 일단 public Hello(Long id, String name)에 접근 가능하도록 ReflectionUtils를 통해서 설정해주었다

결정적인 코드가 도출되었다
@Getter
@AllArgsConstructor
static class Hello {
private Long id;
private String name;
}
현재 Hello Class의 구성은 위와 같고 @AllArgsConstructor을 가지고 있다
이에 따라서 BeanUtils의 instantiateClass에서는 생성자 : public Hello(Long id, String name) / 요청 데이터 : [id: 1, name: "spring"]을 이용하여 Constructor의 newInstance을 호출한다
Constructor의 newInstance를 통해서 완성된 Hello(id: 1, name: "spring") 인스턴스가 만들어진다
간단한 Reflection Test
@Test
void test() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Object[] argsWithDefaultValues = {12345L, "Hello Spring World"};
Constructor<TestController.Hello> constructor = TestController.Hello.class.getConstructor(Long.class, String.class);
constructor.setAccessible(true);
TestController.Hello helloWorld = constructor.newInstance(argsWithDefaultValues);
System.out.println(helloWorld.getId());
System.out.println(helloWorld.getName());
assertThat(helloWorld.getId()).isEqualTo(12345L);
assertThat(helloWorld.getName()).isEqualTo("Hello Spring World");
}

6. 결론

ModelAttributeMethodProcessor의 createAttribute는 Hello(id: 1, name: "spring")를 정제된 결과로써 받고 ArgumentResolver의 resolveArgument의 결과는 Hello(id: 1, name: "spring")로 마무리된다
의문점
하지만 여기서 다음과 같은 의문이 들 수 있다
어떤 블로그에서는 @ModelAttribute로 바인딩할때 setter가 없으면 안된다던데?
- 이미 위에서 이런 의견에 대한 반례를 보였다
요청으로 들어온 데이터에 대한 DTO Binding을 다음 예시 상황을 통해서 알아보자
1. Default Constructor + getter + setter
@Getter
@Setter
static class Hello {
private Long id;
private String name;
}
일단 DTO에 기본 생성자 + setter가 존재할 경우 진행되는 프로세스는 다음과 같다
- 여기서 @NoArgsConstructor가 없는데 기본 생성자가 어디있냐?라고 생각하는 사람 또한 존재할 수 있다
- 위에서도 말했듯이 컴파일러는 클래스 내부적으로 생성자가 없을 경우 파라미터가 없는 기본 생성자 : public Hello(){}를 만들어준다

표시된 해당 블럭에서 ClassType에 대한 인스턴스를 생성한다
- 물론 Object 내부 필드들은 ReferenceType = null / PrimitiveType = default value를 가지게 된다
그 이후 DataBinder에 의해서 값들이 binding되는것이다



1. 기본 생성자에 의해서 Hello (id: null, name: null) 객체 생성
2. DataBinder의 setValue를 통해서 setId, setName -> 완전한 Hello (id: 1L, name: "spring") 객체 생성
2. AllArgsConstructor + getter + setter

상세한 프로세스는 위에서 이미 디버깅을 통해서 확인하였기 때문에 넘어가고 중요한 부분만 살펴보자
모든 필드를 갖는 생성자가 존재하기 때문에 위의 로직 상에서 BeanUtils.instantiateClass를 통해서 Hello(id: 1, name: "spring) 객체가 탄생한다

그 이후 ModelAttributeMethodProcessor의 resolveArgument로 흐름이 넘어와서 진행되다가 bindRequestParameters를 호출함을 확인할 수 있다



마찬가지로 DataBinder로 들어와서 setter로 Binding됨을 확인할 수 있다
1. @AllArgsConstructor, Constructor.newInstance(args...)를 통해서 Hello (id: 1L, name: "spring") 객체 생성
2. DataBinder의 setValue를 통해서 setId, setName -> Hello (id: 1L, name: "spring")
→ 단순하게 객체 내부 필드 값을 덮어씌우는 것이다
3. AllArgsConstructor + getter
모든 필드가 있는 생성자만 존재하고 setter가 없는 상황은 위에서 디버깅을 통해서 파악을 하였다
@AllArgsConstructor, Constructor.newInstance(args...)를 통해서 Hello (id: 1L, name: "spring") 객체 생성
그런데 여기서 추가적으로 확인할 부분은 다음과 같다
- setter가 없는데 DataBinder로 흐름이 넘어가는지
- 만약 DataBinder로 흐름이 넘어간다면 어떤 형식으로 Binding되는지

결론적으로 말하면 DataBinder의 setValue로 흐름이 넘어가지 않는다
- 그 이유는 위의 코드에서 보이는 PropertyHandler의 특정 속성 때문이다
setter 존재 O
@Getter
@Setter
@AllArgsConstructor
static class Hello {
private Long id;
private String name;
}

setter가 열려있기 때문에 PropertyHandler의 writable값이 true이고 그에 따라서 DataBinder의 setValue 부분으로 흐름이 넘어간다
setter 존재 X
@Getter
@AllArgsConstructor
static class Hello {
private Long id;
private String name;
}

setter가 닫혀있기 때문에 PropertyHandler의 writable값이 false이고 그에 따라서 DataBinder의 setValue 부분으로 흐름이 넘어가지 않는다
4. Default Constructor + getter
그러면 기본 생성자만 존재하고 setter가 없을 경우 어떤 상황이 벌어질지 한번 결과로 살펴보자
@Getter
static class Hello {
private Long id;
private String name;
}

Hello의 모든 필드가 null로 바인딩 됨을 확인할 수 있다
이 이유는 다음 흐름을 통해서 알아보자

- 일단 createAttribute에서 Hello는 현재 기본 생성자만 보유하고 있기 때문에 Hello(id: null, name: null)을 반환하게 된다

- 그 이후 resolveArgument 프로세스로 돌아와서 bindRequestParameters 흐름으로 넘어간다

바로 이 시점에서 위의 실험 결과에 따라 setter가 존재하지 않기 때문에 PropertyHandler의 writable값은 false로 설정되고 그에 따라서 DataBinder의 setValue로 흐름이 넘어가지 않는 것이다

- 그 결과로 결국 resolveArgument의 최종 결과값은 값이 바인딩 되지 않은 Hello(id: null, name: null)가 되는 것이다
5. Default Constructor + AllArgsConstructor
과연 기본 생성자 + 모든 필드를 가지는 생성자가 동시에 존재할 경우 어떤 프로세스로 진행될지 한번 살펴보자
@Getter
@NoArgsConstructor
@AllArgsConstructor
static class Hello {
private Long id;
private String name;
}


보이는 결과로 알 수 있듯이 생성자가 여러개 존재하면 기본 생성자가 선택된다
@ModelAttribute 처리 흐름 결론
위의 여러 디버깅 및 실험을 통해서 @ModelAttribute 처리 흐름은 다음과 같이 구조화시킬 수 있다
1. @Getter 필수
2. @Getter + @Setter / @Getter + @AllArgsConstructor 조합이면 정상적으로 바인딩
3. 기본 생성자만 존재 or @NoArgsConstructor + @AllArgsConstructor 조합이면 setter는 강제적이다
1. 정상 바인딩 조합
@Getter + @Setter
@Getter + @AllArgsConstructor
@Getter + @Setter + @NoArgsConstructor
@Getter + @Setter + @AllArgsConstructor
@Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor
2. null 바인딩 조합
@Getter
@Getter + @NoArgsConstructor
@Getter + @NoArgsConstructor + @AllArgsConstructor
3. Resolved ~~ Could not find acceptable representation 예외 발생 조합
@Getter가 없는 모든 상황