HttpMessageConverter와 직렬화, 역직렬화
HttpMessageConverter란?
웹 브라우저 같은 클라이언트에서 보여지는 HTML 컨텐츠 렌더링되는 방식은 크게 두가지가 있다.
서버에서 데이터를 포함한 HTML을 만들어서 이 자체를 한번에 클라이언트 쪽으로 주는 방식인 서버 사이드 렌더링이다.(SSR)
서버가 요청을 받으면 클라이언트에 HTML과 JS를 보낸다. 클라이언트는 그걸 받아 렌더링을 하는게 클라이언트 사이드 렌더링이다.(CSR)
HttpMessageConverter
는 CSR을 위해 필요한 데이터를 직렬화, 역직렬 해야할 때 사용한다.
@PostMapping
public ResponseEntity postMember(@Valid @RequestBody MemberDto.Post requestBody) {
Member member = mapper.memberPostToMember(requestBody);
member.setStamp(new Stamp());
Member createdMember = memberService.createMember(member);
URI location = UriCreator.createUri(MEMBER_DEFAULT_URL, createdMember.getMemberId());
return ResponseEntity.created(location).build();
}
이런 상황에서 객체를 JSON 형태로 바꿔주기 위해 메시지 컨버터를 사용하게 된다.
직렬화와 역직렬화
데이터 직렬화는 메모리를 디스크에 저장하거나 네트워크 통신이 가능한 상태로 변환하는 것이다.
데이터 역직렬화는 직렬화와 반대로 디스크에 저장한 데이터를 읽거나, 네트워크 통신으로 받은 데이터를 메모리에 쓸 수 있도록 변환하는 것이다.
이런 과정이 필요한 이유는 우리가 사용하는 데이터 메모리 구조는 크게 2가지로 나뉘는데,
값 형식 데이터: int, float, char 등 값 형식 데이터. 스택에 메모리가 쌓이고 직접 접근이 가능하다.
참조 형식 데이터: 객체 같은 참조 형식 변수를 힙에 메모리에 할당되고, 스택은 이 힙 메모리를 참조하는 구조로 되어 있다.
이중에 값 형식 데이터가 디스크에 저장하거나 통신할 때 쓰인다.
왜 참조 형식 데이터를 사용할 수 없을까?
예를 들면, 객체 A를 만들고 주소값이 0x22라고 가정하자. 이 값을 파일에 포함해서 저장하고 이후에 프로그램을 종료하고 다시 실행해서 주소값 0x22을 가져 오면 기존 A객체의 데이터를 가져올 수 없다.
프로그램이 종료되면 기존에 할당된 메모리는 해제 되어도 없어지기 때문이다. 네트워크 통신도 각 PC마다 사용하고 있는 메모리 공간 주소는 전혀 다르다. 그러므로 내가 다른 PC로 전송한 A객체 데이터는 의미가 없다.
데이터를 받은 PC의 0x22 주소에는 다른 값이 존재하기 때문이다.
그래서 직렬화는 왜 사용하는가?
직렬화를 하게 되면 각 주소 값이 가지는 데이터를 전부 끌어 모아서 값 형식 데이터로 변환 해준다. 직렬화가 된 데이터는 언어에 따라서 text, binary 등의 형태가 되는데, 이런 형태가 됐을 때 저장하거나 통신할 때 파싱이 가능한 유의미한 데이터가 된다. 즉, 사용하고 있는 데이터를 파일 저장 혹은 데이터 통신에서 파싱할 수 있는 유의미한 데이터를 만들기 위함이다.
쉽게 말해서 객체를 저장, 전송할 수 있는 형태로 변환하는 것 이다.
역직렬화는 데이터를 받아서 객체로 변환한다.
데이터 직렬화의 종류
CSV, XML, JSON 직렬화
사람이 읽을 수 있는 형태
저장 공간의 효율성이 떨어지고, 파싱하는 시간이 오래 걸린다.
데이터의 양이 적을 때 주로 사용
요즘은 JSON 형태로 많이 직렬화를 사용
모든 시스템에서 사용 가능
Binary 직렬화
사람이 읽을 수 없는 형태
저장 공간을 효율적으로 사용할 수 있고, 파싱하는 시간이 빠르다.
데이터의 양이 많을 때 주로 사용
모든 시스템에서 사용 가능
ex) 프로토콜 버퍼, apache Avro 등
Java 직렬화
Java 시스템간의 데이터 교환이 필요할 때 사용
HttpMessageConverter 동작 과정
@ResponseBody
를 사용할 때 동작 과정
동작하는 과정은 @ResponseBody
를 사용하게 되면 ViewResolver
대신에 HttpMessageConverter
가 동작하게 된다.
String
같은 기본 문자는 StringHttpMessageConverter
를 사용하고, 객체는 MappingJackson2HttpMessageConverter
를 사용한다.
이외에도 각자 맞는 컨버터들이 등록되어 있어서 이것들을 사용하게 된다.
응답은 클라이언트의 HTTP Accept 헤더와 서버의 컨트롤러 반환 타입 정보를 조합해서 컨버터가 선택된다.
@ResponseBody
의 경우는 이 위에 컨버터를 사용한다고 했지만, 정확하게 말하면
HTTP 요청은 @RequestBody
, HttpEntity(RequestEntity)
HTTP 응답은 @ResponseBody
, HttpEntity(ResponseEntity)
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;
}
canRead
, canWrite
를 통해 읽기, 쓰기 여부를 확인한다. 컨버터는 요청과 응답 두 경우 모두 사용하기 때문이다.
그 이후에는 read
, write
메서드를 통해 읽고 쓰기 동작을 실행한다.
그리고 클래스와 MediaType
을 체크해서 어떤 메시지 컨버터를 사용할지 결정하는데, 우선순위대로 메시지 컨터버 사용을 검토하면서, 조건에 만족하지 못하면 다음으로 넘어간다.
대표적으로 컨버터는 이렇게 있다.(우선순위대로 나열)
ByteArrayHttpMessageConverter
byte[]
데이터를 처리하고, 클래스 타입은byte[]
이며 MediaType은*/*
이다.
StringHttpMessageConverter
String 문자로 데이터를 처리하고, 클래스 타입은 String, MediaType은
*/*
이다.
MappingJackson2HttpMessageConverter
json으로 데이터를 처리하고, 클래스 타입은 객체 또는 HashMap이며, MediaType은 application/json 관련이다.
참고로 */*
는 모든 미디어 타입을 말한다.
적용
메시지 컨버터는 어디서 적용이 될까?
DispatcherServlet은 그림같은 형태로 동작을 한다.
@RequestMapping
어노테이션이 핸들러 매핑과 핸들러 어댑터에 대한 정보를 주게 되는데, 컨버터는 이 핸들러 어댑터를 수행하는 과정에서 이루어진다.
어댑터에서 이 핸들러를 실행할 때 ArgumentResolver
를 이용해 파라미터를 넘겨준다.
전에 ArgumentResolver
를 정리하면서 했던 것의 핵심은 @Pathvariable
, @RequestBody
등이 이걸 통해 데이터를 넘겨주었던 것이었는데,
그 과정에서 어노테이션 기반 컨트롤러를 처리하는 RequestMappingHandlerAdapotor
는 ArgumentResolver
를 호출해서 컨트롤러(핸들러)가 필요로 하는 다양한 파라미터의 값(객체)을 생성한다. ArgumentResolver
가 필요한 파라미터 값을 반환 해주면 핸들러(컨트롤러)를 호출하면서 값을 넘긴다.
컨버터는 HandlerMethodArgumentResolver
와 HandlerMethodReturnValueHandler
의 과정에서 이용을 하는 것이었다.
HandlerMethodArgumentResolver
: 줄여서ArgumentResolver 이라고 부른다.
HandlerMethodReturnValueHandler
: 줄여서 ReturnValueHandle 이라고 부른다.
이런 과정이 흐르게 된다.
참고 자료
Last updated