(SERVLET) @RequestBody는 어떻게 동작할까?
@RequestBody는 setter 메서드가 없더라도 요청한 json 데이터를 DTO에 매핑하여 응답 본문을 작성한다.
이것이 가능한 이유를 디버깅을 통해 알아보자!
DispatcherServlet에서 시작한다. 요청한 json 데이터를 DTO에 매핑하는 과정은 요청을 처리할 수 있는 핸들러와 어댑터를 찾아온 이후에 시작된다.
핸들러와 어댑터를 찾는 과정에 대해서 궁금하다면 이전 포스팅을 참고하길 바란다.
예시 코드
컨트롤러
@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
References
https://docs.spring.io/spring-framework/docs/3.0.0.M4/spring-framework-reference/html/ch15s02.html