[Spring] @RequestBody 처리 흐름

 

[Spring] @ModelAttribute 처리 흐름

ArgumentResolver Spring Framework에서 ArgumentResolver는 굉장히 중요한 핵심 개념이다 사용자의 Request가 들어오는 순간 처리 메커니즘을 간단하게 알아보자 1. 사용자의 Request가 DispatcherServlet으로 들어온다

sjiwon-dev.tistory.com

이전에는 @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가지 핵심 메소드가 존재한다

  1. canRead → 해당 HttpMessageConverter가 메시지를 다음 기준을 통해서 읽을 수 있는지 판단
  2. canWrite → 해당 HttpMessageConverter가 메시지를 다음 기준을 통해서 쓸 수 있는지 판단
  3. read → 메시지 읽기
  4. write → 메시지 쓰기

 

canRead & canWrite에서는 다음 2가지 기준을 통해서 HttpMessageConverter를 결정한다

  1. Class → 해당 클래스 타입을 지원하는지
  2. 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 Datacom.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

AbstractMessageConverterMethodArgumentResolver - validateIfApplicable
DataBinder - validate

이 과정에서는 @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]