HandlerMethodArgumentResolver
이 HandlerMethodArgumentResolver
에 정의되어있는 자바독을 읽으면
주어진 요청의 컨텍스트에서 메소드 매개변수를 인수 값으로 해석하기 위한 전략 인터페이스라고 설명되어 있다.
이 HandlerMethodArgumentResolver
에는
이렇게 두개의 메소드가 있는데 supportsParameter()
로 메소드의 매개변수를 처리할 수 있는지 여부를 판단한다.
@RequestBody
@RequestBody
어노테이션을 읽으려면
HandlerMethodArgumentResolver
AbstractMessageConverterMethodArgumentResolver
AbstractMessageConverterMethodProcessor
RequestResponseBodyMethodProcessor
이순서로 확장되어있는 Resolver
를 찾아보면 된다.
저번 포스팅에서 봤던 ArgumentResolver
들을 포함한 RequestMappingHandlerAdapter
에서는
HandlerMethodArgumentResolverComposite
를 주입받게 된다.
HandlerMethodArgumentResolverComposite
에서도
맞는 ArgumentResolver
를 찾아서 동작하게 하려고 하는데 27개의 리졸버중에
RequestResponseBodyMethodProcessor
가 @RequestBody
를 처리해준다.
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
}
그리고 이제 이 객체를 바인딩 해야한다.
MessageConverter
HandlerMethodArgumentResolverComposite
에서 메세지 컨버터를 10개중에 또 찾아내야 한다.
그거는 아래에서 설명하겠다.
HttpInputMessage에서 들어온 키값과 @RequestBody
가 붙은 객체 인자들 값을 서로 비교해서
맞으면 매핑을 시켜주는 것 같다.
AbstractMessageConverterMethodArgumentResolver
AbstractMessageConverterMethodArgumentResolver
에서 MediaType
과
메세지 컨버터를 맞는걸 찾는데 걸리는 컨버터는 바로 MappingJackson2HttpMessageConverter
이다.
예상은 하고 있었지만 왜 저 클래스가 GenericHttpMessageConverter
인지는 아직 잘 모르겠다.
메소드중 위 기능을 하는 일부를 가져와보았다.
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
boolean noContentType = false;
try {
contentType = inputMessage.getHeaders().getContentType();
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
Class<?> contextClass = parameter.getContainingClass();
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = (Class<T>) resolvableType.resolve();
}
HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
Object body = NO_VALUE;
EmptyBodyCheckingHttpInputMessage message;
try {
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
for (HttpMessageConverter<?> converter : this.messageConverters) { //여기에 아까 말했던 Converter가 10개 들어오게 된다.
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) { //여기가 참 조건이 성립하려면 위에서 GenericHttpMessageConverter 클래스 유형이어야 한다.
HttpInputMessage msgToUse = //여기는 메소드 이름을 보면 body를 읽기 전,후 로 나뉘어 있어 aop동작을 하는것으로 짐작된다.
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
}
else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
}
catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}
if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
(noContentType && !message.hasBody())) {
return null;
}
throw new HttpMediaTypeNotSupportedException(contentType,
getSupportedMediaTypes(targetClass != null ? targetClass : Object.class));
}
MediaType selectedContentType = contentType;
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
});
return body;
}
}
이렇게 해서 커맨드 객체 @RequestBody
가 붙은 HelloForm유형에 맞는
데이터들을 파싱해주고 처리를 해줄 수 있게 된다.
번외로 AbstractMessageConverterMethodProcessor
의 구현체는
RequestResponseBodyMethodProcessor
, HttpEntityMethodProcessor
이므로
새롭게 알게된 사실인데 @RequestBody
를 붙이고 싶지 않다면 HttpEntity<T>
나 RequestEntity<T>
로 매개변수를 받아주면
HttpEntityMethodProcessor
가 동작하고, 가공이 조금 덜 되었지만 그래도 그안에 커맨드 객체까지 담아오는 body 데이터를 받을 수 있다.
정리
내 수준으로는 아직 이것밖에 이해 못했지만 그래도 얼추 디버깅으로 바짝 쫓아갈 수는 있다고 생각한다.
예전엔 이걸 봐도 어떻게 돌아가는지 무지성으로 넘기기만 했지, 자세하게 들여다 볼 실력도 안됐었다.
근데 지금은 천천히 늦지만서도 찍어보면서 어떻게 흐름이 진행되는지는 감을 익히는 것 같다.
이 부분도 자바 공부를 더 하다보면 깊게 알 수 있게 되지 않을까 싶다.
HelloController.java
@RestController
@RequestMapping("/api/v1")
public class HelloController {
@PostMapping("/hello")
public ResponseEntity<?> hello(@RequestBody HelloForm helloForm) {
return ResponseEntity.ok(Map.of("message", "success","data", helloForm));
}
}
HelloControllerTest.java
@WebMvcTest(HelloController.class)
class HelloControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void test() throws Exception {
String ss = "{\"message\": \"hello\", \"name\": \"lsj\"}";
mockMvc.perform(post("/api/v1/hello")
.content(ss)
.contentType(MediaType.APPLICATION_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk());
}
}
HelloForm.java
@Getter //없으면 406에러를 발생한다. 깂을 못읽어 주입을 못해주는것 같다.
@NoArgsConstructor
public class HelloForm {
private String message;
private String name;
//없으면 커맨드 객체 주입 안됨
public HelloForm(String message, String name) {
this.message = message;
this.name = name;
}
}