Spring/Spring MVC

(SERVLET) @RequestBody는 어떻게 동작할까?

주누 2022. 7. 8. 16:27

@RequestBody는 setter 메서드가 없더라도 요청한 json 데이터를 DTO에 매핑하여 응답 본문을 작성한다.
이것이 가능한 이유를 디버깅을 통해 알아보자!

 

DispatcherServlet에서 시작한다. 요청한 json 데이터를 DTO에 매핑하는 과정은 요청을 처리할 수 있는 핸들러와 어댑터를 찾아온 이후에 시작된다.
핸들러와 어댑터를 찾는 과정에 대해서 궁금하다면 이전 포스팅을 참고하길 바란다.
 

(SERVLET) DispatcherServlet의 내부 살펴보기

DispatcheServlet의 doService부터 시작 핸들러 찾기 요청을 찾아올 수 있는 핸들러를 찾아오는 부분 핸들러 매핑 찾기 - 기본으로 두 개의 핸들러 매핑이 제공된다. 두 가지 핸들러 중 찾은 핸들러는 Req

jwdeveloper.tistory.com

예시 코드

컨트롤러

@RestController
@RequestMapping(value = "/request-body")
public class RequestController {

    @GetMapping("/used")
    public ResponseEntity<RequestDto> useRequestBodyTest(@RequestBody RequestDto requestDto) {
        return ResponseEntity.ok(requestDto);
    }

}

테스트 코드

@ExtendWith(value = MockitoExtension.class)
@ActiveProfiles("test")
public class RequestControllerUnitTest {

    private static final String PATH = "/request-body";
    private MockMvc mockMvc;
    private ObjectMapper objectMapper;
    @InjectMocks
    private RequestController requestController;

    @BeforeEach
    void setUp() {
        setUpMockMvc();
        setUpObjectMapper();
    }

    private void setUpObjectMapper() {
        objectMapper= new ObjectMapper().registerModule(new JavaTimeModule());
    }

    private void setUpMockMvc() {
        mockNonUseInitBinderMvc = mockMvc.standaloneSetup(requestController)
                .build();
    }

    @Test
    @DisplayName("@RequestBody O - QueryString Param Test")
    public void controller_content_test() throws Exception {
        RequestDto requestDto = new RequestDto("junwoo", 1, 2);
        String requestBody = objectMapper.writeValueAsString(requestDto);
        String responseBody = objectMapper.writeValueAsString(requestDto);
        ResultActions perform = mockMvc.perform(get(PATH + "/used")
                .contentType(MediaType.APPLICATION_JSON)
                .content(requestBody));

        perform.andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(content().string(responseBody))
                .andDo(print());
    }

}

동작 방식

DispatcherServlet

DispatcherServlet에서 찾아온 핸들러와 어댑터를 가지고 아래 부분에서 실제 요청을 처리한다.

 

InvocableHandlerMethod

RequestMappingHandlerAdapter와 ServletInvocableHandlerMethod를 거친 이후에 InvocableHandlerMethod에서 매핑할 값을 가져와서 DTO에 매핑하는 작업을 진행하게 된다.

  • getMethodArgumentValues : body 데이터를 가져오기
  • doInvoke : body 데이터 DTO 매핑

supportsParameter를 통해 해당 파라미터를 처리할 수 있는 rosolver를 조회한다.
실제 조회하는 역할은 HandlerMethodArgumentResolverComposite가 담당한다.

 

HandlerMethodArgumentResolverComposite

27개의 Resolver 중 파라미터 타입 및 명시된 어노테이션 정보를 처리할 수 있는 Resolver를 조회한다. (@RequestBody RequestDto requestDto)

@RequestBody를 처리할 수 있는 Resolver는 RequestResponseBodyMethodProcessor이다.

조회 이후 아래 정보를 기준으로 캐시에 저장한다.

key value
파라미터 정보 RequestResponseBodyMethodProcessor

 

 

RequestResponseBodyMethodProcessor

RequestResponseBodyMethodProcessor에서는 상위 추상 클래스인 AbstractMessageConverterMethodArgumentResolver에게 위임하는 역할을 담당한다.

AbstractMessageConverterMethodArgumentResolver

8가지의 messageConverter를 순회한 다음 요청한 contentType과 파라미터를 읽을 수 있는 messageConverter를 찾는다.

@RequestBody에 해당하는 messageConverter는 MappingJackson2HttpMessageConverter이다.

 

AbstractJackson2 HttpMessageConverter

ObjectMapper의 readValue() 메서드를 통해 body의 내용을 읽어드려 json key값들을 해당 타입의 필드에 바인딩을 시켜준다.

ObjectMapper는 getter 메서드만 존재해도 prefix 정보를 떼어낸 채 요청 json의 key값과 매핑하기 때문에 setter가 필요 없는 것이다.

아래와 같이 body에 요청한 데이터가 담긴 것을 볼 수 있다.

 

다시 InvocableHandlerMethod

  • getMethodArgumentValues : body 데이터를 가져오기
  • doInvoke : body 데이터 DTO 매핑

이제 doInvoke를 통해서 body 데이터를 DTO에 매핑하는 작업만 남았다.

getMethodArgumentValues에서 가져온 body 정보를 리플렉션을 통해서 컨트롤러 메서드의 파라미터로 넣어준다.

확인


정리

  • @RequestBody를 처리할 수 있는 Resolver를 가져온다. (RequestResponseBodyMethodProcessor)
  • 해당 Resolver에 등록되어 있는 Converter 중 MappingJackson2HttpMessageConverter 에서 body 정보를 생성한다.
    • 생성 시 ObjectMapper 활용
  • 리플렉션을 통해 가져온 body 값을 컨트롤러의 파라미터에 invoke 해주면서 데이터가 바인딩된다.

Code Link

https://github.com/mike6321/TIL/commit/970023fa853be57f13e94778a9549544e95879eb

 

[junwoo.choi] feat : (testCode) queryParam initBinding · mike6321/TIL@970023f

Show file tree Hide file tree Showing 4 changed files with 157 additions and 0 deletions.

github.com

References

https://docs.spring.io/spring-framework/docs/3.0.0.M4/spring-framework-reference/html/ch15s02.html

 

15.2 The DispatcherServlet

15.2 The DispatcherServlet Spring's web MVC framework is, like many other web MVC frameworks, request-driven, designed around a central servlet that dispatches requests to controllers and offers other functionality that facilitates the development of web

docs.spring.io

https://jenkov.com/tutorials/java-json/jackson-objectmapper.html#how-jackson-objectmapper-matches-json-fields-to-java-fields