Spring

스프링 HTTP 메시지 컨버터

monkeyDugi 2022. 2. 14. 22:34
반응형

HTTP 메시지 컨버터

뷰 템플릿으로 HTML을 생성해서 응답하지 않고, HTTP API처럼 JSON 데이터를
HTTP 바디에서 직접 읽거나 쓰는 경우 HTTP 메시지 컨버터를 사용하면 편리하다.

HTTP 메시지 컨버터란?


  • 요청 및 응답이 왔을 경우 HttpServlet등을 하위 레벨을 사용하면 직접 request, response 객체에 write도 하고
    여러가지 작업을 직접 해야하므로 불편하기 때문에
    이를 편리하게 사용할 수 있도록 @ReqeustBody, @ResponseBody 등을 읽어
    사용하기 편한 형태로 제공하는 컨버터이다.

스프링 MVC는 어떤 상황에 HTTP 컨버터를 사용할까?


  • @ReqeustBody, HttpEntity(RequestEntity)
  • @ResponseBody, HttpEntity(ResponseEntity)

이 상황일 메시지 컨버터를 사용하게 된다. 그렇지 않은 경우 뷰 템플릿이나, 정적 리소스를 제공한다.

HttpMessageConverter 인터페이스

  • 사용되는 인터페이스이다.
public interface HttpMessageConverter<T> {

    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

    List<MediaType> getSupportedMediaTypes();

    default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
        return (canRead(clazz, null) || canWrite(clazz, null) ?
                getSupportedMediaTypes() : Collections.emptyList());
    }

    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;

    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;
}
  • HTTP요청, 응답 모두 사용됨.
  • canRead(), canWrite()는 해당 클래스와 미디어 타입(content-type)을 지원하는지 체크한다.
  • read(), write()는 메시지를 읽고 쓴다.

동작 방식

스프링 부트 기본 메시지 컨버터

// 일부 생략
0 = ByteArrayHttpMessageConverter
1 = StringHttpMessageConverter
2 = MappingJackson2HttpMessageConverter

우선 순위로 클래스 타입과 미디어 타입을 체크하고 없다면 다음 순서로 넘어간다.

**ByteArrayHttpMessageConverter**

  • byte[] 데이터 처리
  • 클래스 타입 : byte[], 미디어 타입 : */*(모두)
  • 요청 예) @ReqeustMBody byte[] data
  • 응답 예) @ResponseBody return byte[]
    → 쓰기 미디어 타입(application/octet-stream),
  • 미디어 타입이 모두이기 때문에 클래스 타입만 일치하면 됨.

**StringHttpMessageConverter**

  • String 문자열로 데이터 처리
  • 클래스 타입 : String, 미디어 타입 : */*(모두)
  • 요청 예) @RequestBody String data
  • 응답 예) @ResponseBody return “ok”
    → 쓰기 미디어 타입 : text/plain
  • 미디어 타입이 모두이기 때문에 클래스 타입만 일치하면 됨.
// 미디어 타입은 모두 허용이기 때문에 클래스 타입인 String만으로 선택 됨.
content-type: application/json

@RequestMapping
void hello(@RequetsBody String data) {}

**MappingJackson2HttpMessageConverter**

  • application/json 데이터 처리
  • 클래스 타입 : 객체 or HashMap, 미디어 타입 : application/json
  • 요청 예) @ReqeustBody HelloData data
  • 응답 예) @ResponseBody return helloData
    → 쓰기 미디어 타입 : application/json

미디어 타입이 application/json이기 때문에 클래스 타입과 미디어 타입 모두 일치해야 함.

// 미디어 타입과 클래스 타입 모두 일치할 시 선택 됨.
content-type: application/json

@RequestMapping
void hello(@RequetsBody HelloData data) {}

**예외 상황**

// 미디어 타입과 클래스 타입 모두 맞지 않음.
content-type: text/html

@RequestMapping
void hello(@RequetsBody HelloData data) {}

HTTP 요청 데이터 읽기

  • HTTP 요청이 오고, 컨트롤러에서 @RequestBody, HttpEntity 파라미터를 사용해 반환한다.
  • 메시지 컨버터가 확인을 위해 canRead() 호출
    • 대상 클래스 타입을 지원하는가
      • 예) @RequestBody의 대상 클래스(byte[], String, HelloData)
    • HTTP 요청의 Content-Type 미디어 타입을 지원하는가
      • 예) text/plain, application/json, */*
  • canRead() 조건을 만족하면 read()를 호출해서 객체 생성하고 반환.

정리
→ HTTP 요청이 오면 컨트롤러에서 @RequestBody, HttpEntity이 있다면,
대상 클래스 타입과 미디어 타입을 체크하여 맞는 컨버터를 반환한다.

**HTTP 응답 데이터 생성**

  • 컨트롤러에서 @ResponseBody, HttpEntity로 값이 반환.
  • 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해 canWrite()를 호출한다.
    • 대상 클래스 타입을 지원하는가
      • 예) return의 대상 클래스(byte[], String, HelloData)
    • HTTP 요청의 Accept 미디어 타입을 지원하는가.(더 장확히는 @RequestMapping의 produces)
      • 예) text/plain, application/json, */*
  • canWrite()조건을 만족하면 write()를 호출해서 HTTP 응답 메시지 바디에 데이터 생성

정리

→ @ResponseBody나 HttpEntity가 있다면, 대상 클래스 반환 타입과 미디어 반환 타입을
체크하여 맞는 컨버터를 반환 반환한다.

HTTP 메시지 컨버터는 스프링 MVC의 어디쯤에서 사용되는 걸까?

이 그림에서는 보이지 않는다.


출처

바로 @RequestMapping을 처리하는 핸들러 어댑터인 RequestMappingHandlerAdapter에서 사용된다.
ArgumentResolver와 ReturnValueHandler를 살펴보자.


출처

우리는 애노테이션 기반 컨트롤러에서
HttpServletRequest , Model 은 물론이고, @RequestParam , @ModelAttribute 같은 애노테이션
그리고 @RequestBody , HttpEntity 같은 HTTP 메시지를 처리하는 부분까지 매우 큰 유연함을 보여주었다.
이 역할을 해주는 것이 ArgumnetResolver, ReturnValueHandler이다.

애노테이션 핸들러를 처리하는 RequestMappingHandlerAdapter가 ArgumnetResolver를 호출하여
핸들러가 필요로 하는 다양한 값(객체)을 생성한다. 이후 셋팅이 되면 컨트롤러를 호출하여 값을 넘겨주게 된다.

스퍼링은 30개가 넘는 ArgumentResolver를 제공한다.

가능한 파라미터 참고할 공식 메뉴얼 참고
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments

ArgumnetResolver 동작 방식

정확히는 HandlerMethodArgumentResolver 인데 줄여서 ArgumentResolver 라고 부른다.

public interface HandlerMethodArgumentResolver {

      boolean supportsParameter(MethodParameter parameter);

            @Nullable
      Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory, binderFactory) throws Exception;
}
  • ArgumnetResolversupportsParameter()를 호출해서 해당 파라미터(@ModelAttribute 등)를 지원하는지 체크한다.
  • 지원하면 resolveArgumnet()를 호출해서 실제 객체를 생성한다.
  • 이 객체가 컨트롤러 호출 시 넘어가게 된다.

ReturnValueHandler

HandlerMethodReturnValueHandler 를 줄여서 ReturnValueHandle 라 부른다.
ArgumentResolver 와 비슷한데, 이것은 응답 값을 변환하고 처리한다.

컨트롤러에서 String으로 뷰 이름을 반환해도, 동작하는 이유가 바로 ReturnValueHandle 덕분이다.

스프링은 약 10개가 넘는 ReturnValueHandle를 지원한다.
예)ModelAndView, @ResponseBody, HttpEntity, String

가능한 파라미터 참고할 공식 메뉴얼 참고

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-return-types

HTTP 메시지 컨버터 위치


출처

위에서 ArgumnetResolver와 ReturnValueHandler가 사용할 객체를 생성 해준다고 했다.
하지만 분명 HTTP 메시지 컨버터가 각 컨버터들을 이용해서 객체를 생성해준다고 했다.
즉, 내부적으로 ArgumnetResolver와 ReturnValueHandler는 HTTP 메시지 컨버터를 호출하여
객체를 생성해 주는 것이다. @ReuqestBody, @ResonseBody, HttpEntity에 한해서 메시지 컨버터를 호출한다.
즉, 다른 것들은 HTTP 메시지 컨버터를 통하지 않고 처리하게 된다.

요청의 경우 @RequestBody 를 처리하는 ArgumentResolver 가 있고, HttpEntity 를 처리하는
ArgumentResolver 가 있다. 이 ArgumentResolver 들이 HTTP 메시지 컨버터를 사용해서 필요한
객체를 생성하는 것이다.
응답의 경우 @ResponseBodyHttpEntity 를 처리하는 ReturnValueHandler 가 있다. 그리고
여기에서 HTTP 메시지 컨버터를 호출해서 응답 결과를 만든다.

ArgumnetResolver가 HTTP 메시지 컨버터를 사용하는 코드를 간단하게 살펴보자.
HttpEntity를 지원하는 구현체를 골랐다. 코드는 인터페이스에서 정의한 메서드만 간략히 정리했다.
주석을 살펴보자. ReturnValueHandler도 비슷하다.

public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {

            // 1, 넘어온 파라미터가 지원하는 타입인지 체크한다.
            @Override
            public boolean supportsParameter(MethodParameter parameter) {
                return (HttpEntity.class == parameter.getParameterType() ||
                        RequestEntity.class == parameter.getParameterType());
            }

            @Override
            @Nullable
            public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                    NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
                    throws IOException, HttpMediaTypeNotSupportedException {
                ...
                // 2. 객체를 생성받기 위해 HTTP 메시지 컨버터에 위임한다.
                Object body = readWithMessageConverters(webRequest, parameter, paramType);
                ...
            }

            @Nullable
            protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
                    Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

                HttpInputMessage inputMessage = createInputMessage(webRequest);
                // 3. 또 호출
                return readWithMessageConverters(inputMessage, parameter, paramType);
            }

            @Nullable
            protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
                                Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

                    EmptyBodyCheckingHttpInputMessage message;
                            try {
                                message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

                                for (HttpMessageConverter<?> converter : this.messageConverters) { // 메시지 컨버도 루프
                                        ...
                                            (targetClass != null && converter.canRead(targetClass, contentType))) { // 4. 메시지 컨버터 지원하는지 체크                                    
                                            ...
                                            ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse)); // 5. 지원되면 읽어서 반환하기.
                                        ...
                                    }
                                }
                            }
            }
}

WebMvcConfigurer


이걸 많이 보긴 봤는데 뭔지 몰랐었는데 MVC에서 제공하지 않는 새로운 기능을 커스텀하기 위한 것이었다.
요걸 상속 받아서 스프링 빈으로 등록해서 사용한다고 한다.
아주 여러가지를 확장하도록 제공된다.

ArgumentResolvers 커스텀하기

스프링은 여러 인터페이스를 제공하기 때문에 많은 구현체를 커스텀하여 사용할 수 있다.
그 중 ArgumentResolver를 커스텀 해보도록 하자.

ToDo

  • 컨트롤러에서 @AuthenticationPrincipal을 사용하도록 커스텀 추가하기.

컨트롤러에서 사용될 코드는 아래와 같다.

@GetMapping("/members/me")
public ResponseEntity<MemberResponse> findMemberOfMine(@AuthenticationPrincipal LoginMember loginMember) {
    return ResponseEntity.ok().body(new MemberResponse(loginMember.getId(), loginMember.getEmail(), loginMember.getAge()));
}

스프링은 WebMvcConfigurer을 구현하연 여러 커스텀을 추가할 수 있는데 ArgumentResolvers도 이에 포함된다.
addArgumentResolvers()를 오버라이드만 해서 add() 해주고 구현체만 넣어주면 된다.

@Configuration
public class AuthConfig implements WebMvcConfigurer {
    ...
    @Override
    public void addArgumentResolvers(List argumentResolvers) {
        argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
    }
        ...

이제 구현체를 만들어 보자.
스프링은 HandlerMethodArgumentResolver로 이용하여 파라미터를 매핑하게 된다.
그래서 이를 구현해주면 된다.

  • supportsParameter()
    → 파라미터 타입이 맞는지 체크한다.
  • resolveArgument()
    → 파라미터 타입이 맞으면 로직을 처리하여 매핑시켜 제공한다.
    해당 코드에서 getPrincipal()은 하위 타입이 LoginMember이기 때문에 컨트롤러에서 오토 다운 캐스팅이 되기 때문에 사용이 가능하다.
public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(AuthenticationPrincipal.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
        return authentication.getPrincipal();
    }
}

이런 식으로 커스텀하여 사용하여 사용할 수 있다.
컨트롤러에서 복잡한 코드가 필요없게 된다. 복잡한 안좋은 코드를 보자.
아래와 같이 직접 파라미터를 가져오는 행위를 할 필요가 없다.
즉, 역할이 분리된 것이다.

반응형