728x90

HandlerMethodArgumentResolver

HandlerMethodArgumentResolver에 정의되어있는 자바독을 읽으면

주어진 요청의 컨텍스트에서 메소드 매개변수를 인수 값으로 해석하기 위한 전략 인터페이스라고 설명되어 있다.

HandlerMethodArgumentResolver에는

스크린샷 2021-11-29 오후 10 09 59

이렇게 두개의 메소드가 있는데 supportsParameter()로 메소드의 매개변수를 처리할 수 있는지 여부를 판단한다.

@RequestBody

@RequestBody 어노테이션을 읽으려면

HandlerMethodArgumentResolver
AbstractMessageConverterMethodArgumentResolver
AbstractMessageConverterMethodProcessor
RequestResponseBodyMethodProcessor

이순서로 확장되어있는 Resolver를 찾아보면 된다.

저번 포스팅에서 봤던 ArgumentResolver들을 포함한 RequestMappingHandlerAdapter 에서는

HandlerMethodArgumentResolverComposite를 주입받게 된다.

HandlerMethodArgumentResolverComposite에서도

맞는 ArgumentResolver를 찾아서 동작하게 하려고 하는데 27개의 리졸버중에

RequestResponseBodyMethodProcessor@RequestBody를 처리해준다.

스크린샷 2021-11-29 오후 10 24 32
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(RequestBody.class);
    }
}

그리고 이제 이 객체를 바인딩 해야한다.

MessageConverter

HandlerMethodArgumentResolverComposite에서 메세지 컨버터를 10개중에 또 찾아내야 한다.

그거는 아래에서 설명하겠다.

스크린샷 2021-11-29 오후 10 31 17

HttpInputMessage에서 들어온 키값과 @RequestBody가 붙은 객체 인자들 값을 서로 비교해서

맞으면 매핑을 시켜주는 것 같다.

AbstractMessageConverterMethodArgumentResolver

AbstractMessageConverterMethodArgumentResolver에서 MediaType

메세지 컨버터를 맞는걸 찾는데 걸리는 컨버터는 바로 MappingJackson2HttpMessageConverter이다.

예상은 하고 있었지만 왜 저 클래스가 GenericHttpMessageConverter 인지는 아직 잘 모르겠다.

메소드중 위 기능을 하는 일부를 가져와보았다.

스크린샷 2021-11-29 오후 10 46 47

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;
    }

}
728x90

'디버깅' 카테고리의 다른 글

AWS SNS 토큰 에러  (0) 2022.08.10
YAML 파일을 읽어보자  (0) 2022.08.09
RequestParamMethodArgumentResolver  (0) 2022.08.09
RequestMapping 동작  (0) 2022.08.09

+ Recent posts