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가 없는 모든 상황