이전에는 @ModelAttribute에 대한 처리 흐름에 대해서 알아보았다
이번에는 @ModelAttribute와 마찬가지로 Request Binding에서 굉장히 많이 활용되는 @RequestBody에 대해서 알아보자
@RestController
public class TestController {
@PostMapping("/hello")
public Hello invokeHello(@RequestBody Hello hello) {
return hello;
}
}
@Getter
@AllArgsConstructor
public class Hello {
private Long id;
private String name;
}
HandlerMethodArgumentResolver
@ModelAttribute 디버깅때와 마찬가지로 @RequestBody는 어떤 ArgumentResolver로 resolve되는지 확인해보자
getArgumentResolver의 결과를 보니 RequestResponseBodyMethodProcessor가 선택되었다
- RequestResponseBodyMethodProcessor를 자세히 보면 @RequestBody에 대한 resolveArgument뿐만 아니라 @ResponseBody에 대한 handleReturnValue도 처리해줌을 확인할 수 있다
RequestResponseBodyMethodProcessor
@RequestBody에 대한 Binding처리를 RequestResponseBodyMethodProcessor가 함을 확인하였고 이제 내부 resolveArgument 메커니즘에 대해 알아보자
1. RequestResponseBodyMethodProcessor - resolveArgument
readWithMessageConverters를 통해서 Argument를 정제하는듯 보인다
- 메소드 네이밍을 통해서 MessageConverter가 동작함을 유추할 수 있다
🔎 HttpMessageConverter
Spring에서는 HTTP Request & Response Body Data를 Converting하기 위한 HttpMessageConverter를 제공한다
- ByteArrayHttpMessageConverter
- GsonHttpMessageConverter
- MappingJackson2HttpMessageConverter
- ...
HttpMessageConverter는 4가지 핵심 메소드가 존재한다
- canRead → 해당 HttpMessageConverter가 메시지를 다음 기준을 통해서 읽을 수 있는지 판단
- canWrite → 해당 HttpMessageConverter가 메시지를 다음 기준을 통해서 쓸 수 있는지 판단
- read → 메시지 읽기
- write → 메시지 쓰기
canRead & canWrite에서는 다음 2가지 기준을 통해서 HttpMessageConverter를 결정한다
- Class → 해당 클래스 타입을 지원하는지
- MediaType → 해당 미디어타입을 지원하는지
2. RequestResponseBodyMethodProcessor - readWithMessageConverters
3. AbstractMessageConverterMethodArgumentResolver - readWithMessageConverters
AbstractMessageConverter의 readWithMessageConverters 로직으로 들어오고 난 후 현재 @RequestBody의 대상 Object & ContextClass에 대한 정보를 추출한다
- ContextClass → com.sjiwon.blogcode.TestController (@RestController)
- targetClass → com.sjiwon.blogcode.Hello (@RequestBody)
이 후 HTTP Request Message에서 Content-Type & HTTP Method 정보를 추출한다
- Content-Type → application/json;charset=UTF-8
- HTTP Method → POST
드디어 핵심 Converting 로직에 도달한듯 보인다
수많은 HttpMessageConveter의 구현체 중에서 현재 HTTP Request에 대해서 Class & MediaType을 고려해서 canRead인 HttpMessageConverter를 찾은 후 read를 진행한다
현재 Class & MediaType에 대해서 converting을 진행할 MappingJackson2HttpMessageConverter가 보인다
4. AbstractJackson2HttpMessageConverter - read
JSON Data를 Java Object로 Converting하는 ObjectMapper를 볼 수 있다
- ObjectMapper는 실질적으로 JSON 데이터를 읽고 쓰는 ObjectReader & ObjectWriter에 대한 Factory Class역할을 한다고 볼 수 있다
드디어 ObjectReader를 통해서 현재 HTTP Request Message Body로 들어온 JSON Data를 com.sjiwon.blogcode.Hello로 Converting하는 로직을 볼 수 있다
5. AbstractMessageConverterMethodArgumentResolver - readWithMessageConverters
다시 readWithMessageConverters Context로 돌아와서 ObjectReader로 Parsing한 Body Data에 대해서 값이 비어있는지 확인하는 로직이다
최종적으로 ObjectReader로 Parsing한 데이터를 return하는 것을 볼 수 있다
- Hello[id: 1, name: "spring"]
6. RequestResponseBodyMethodProcessor - resolveArgument
긴 과정끝에 readWithMessageConverters를 통해서 JSON 데이터를 Hello로 Converting하고 그 결과는 arg에 담겨져 있다
이후 WebDataBinder & validateIfApplicable(binder, parameter) 로직으로 들어가게 된다
validateIfApplicable
이 과정에서는 @Valid or @Validated에 의해서 JSON -> Object로 Converting한 여러 값들에 대한 validation을 진행한다
validation을 진행한 후 여러 validation annotation에 대해서 필드 값들이 이 validation 규칙을 지키지 않았을 경우 위와 같이 MethodArgumentNotValidException이 발생하게 된다
- @RequestBody → MethodArgumentNotValidException
- @ModelAttribute → BindException
@RequestBody @Valid Hello hello
@Getter
@AllArgsConstructor
public class Hello {
@NotNull
private Long id;
@NotBlank
private String name;
}
7. HandlerMethodArgumentResolverComposite - resolveArgument
위와 같은 흐름에 의해 @RequestBody Binding이 진행되는 것을 확인할 수 있었다
여러가지 Question
1. Request Message Body가 비어있다면?
HTTP Request Message Body를 비운 상태에서 @RequestBody가 어떤 방식으로 Binding하는지 확인해보자
readWithMessageConverters에서 Converter를 통해서 read하려는 과정에서 message.hasBody()가 false로 도출됨을 확인할 수 있다
따라서 else block의 handleEmptyBody 로직으로 진행된다
그 후 다시 RequestResponseBodyMethodProcessor의 readWithMessageConverters 흐름으로 돌아와서
조건절에 대한 결과가 true로 도출됨에 따라 HttpMessageNotReadableException이 발생하게 된다
Resolved [org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing: public com.sjiwon.blogcode.Hello com.sjiwon.blogcode.TestController.invokeHello(com.sjiwon.blogcode.Hello)]
그러면 @RequestBody(required = false)로 설정하면 어떻게 될까?
@RestController
public class TestController {
@PostMapping("/hello")
public Hello invokeHello(@RequestBody(required = false) Hello hello) {
System.out.println("결과 = " + hello);
return hello;
}
}
arg == null이지만 checkRequired가 false이므로 HttpMessageNotReadableException은 발생하지 않고 null값 그대로 return하는 것을 확인할 수 있다
2. GET + @RequestBody?
@RestController
public class TestController {
@GetMapping("/hello")
public Hello invokeHello(@RequestBody Hello hello) {
return hello;
}
}
GET + @RequestBody 조합이여도 Binding은 정상적으로 이루어짐을 확인할 수 있다
하지만 RFC 7231 문서를 보게 되면 다음과 같이 나와있다
GET 요청에 Request Body가 들어가는거는 별도의 제약은 없겠지만 기존의 구현체에서는 요청을 Reject시킬 수 있다
따라서 웬만하면 GET 요청에 Body Data를 담지 않는것이 좋다고 생각한다
3. Lombok Case에 따른 Binding 결과
(1) Getter + NoArgs
@Getter
public class Hello {
private Long id;
private String name;
}
(2) Getter + AllArgs
@Getter
@AllArgsConstructor
public class Hello {
private Long id;
private String name;
}
(3) Getter + Setter + NoArgs
@Getter
@Setter
public class Hello {
private Long id;
private String name;
}
(4) Getter + Setter + AllArgs
@Getter
@Setter
@AllArgsConstructor
public class Hello {
private Long id;
private String name;
}
(5) Getter + Setter + NoArgs + AllArgs
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Hello {
private Long id;
private String name;
}
(6) no Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Hello {
private Long id;
private String name;
}
Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: No acceptable representation]