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
)
- 예) @RequestBody의 대상 클래스(
- HTTP 요청의 Content-Type 미디어 타입을 지원하는가
- 예) text/plain, application/json,
*/*
- 예) text/plain, application/json,
- 대상 클래스 타입을 지원하는가
canRead()
조건을 만족하면 read()를 호출해서 객체 생성하고 반환.
정리
→ HTTP 요청이 오면 컨트롤러에서 @RequestBody, HttpEntity
이 있다면,
대상 클래스 타입과 미디어 타입을 체크하여 맞는 컨버터를 반환한다.
**HTTP 응답 데이터 생성**
- 컨트롤러에서
@ResponseBody, HttpEntity
로 값이 반환. - 메시지 컨버터가 메시지를 쓸 수 있는지 확인하기 위해
canWrite()
를 호출한다.- 대상 클래스 타입을 지원하는가
- 예) return의 대상 클래스(
byte[], String, HelloData
)
- 예) return의 대상 클래스(
- 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;
}
ArgumnetResolver
의supportsParameter()
를 호출해서 해당 파라미터(@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 메시지 컨버터를 사용해서 필요한
객체를 생성하는 것이다.
응답의 경우 @ResponseBody
와 HttpEntity
를 처리하는 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();
}
}
이런 식으로 커스텀하여 사용하여 사용할 수 있다.
컨트롤러에서 복잡한 코드가 필요없게 된다. 복잡한 안좋은 코드를 보자.
아래와 같이 직접 파라미터를 가져오는 행위를 할 필요가 없다.
즉, 역할이 분리된 것이다.
'Spring' 카테고리의 다른 글
Spring Rest Docs를 사용하는 이유와 사용하지 않을 이유 (0) | 2022.03.26 |
---|---|
스프링에서 싱글톤 빈을 사용해도 위험하지 않은가? (0) | 2022.02.14 |
스프링의 뷰 리졸버 (0) | 2022.01.24 |
스프링의 핸들러 매핑과 어댑터 (0) | 2022.01.24 |
스프링 MVC 구조 알아보기 (0) | 2022.01.24 |