Spring은 Http Message Body를 어떻게 Java의 객체로 역/직렬화할까?

노력을 즐겼던 사람·2021년 9월 29일
6
post-thumbnail

Spring은 어떻게 Http Message Body를 Java의 객체로 역/직렬화할까?

스프링을 다루면서 이런 궁금증이 있었습니다.

리턴 타입이 다른데 Http Body에는 똑같은 값이 들어있네? 이거 누가 이렇게 알잘딱 해주는거지?

스프링 공식문서의 그림을 살펴보겠습니다.

이렇다고 하네요. 조금 더 구체적인 상황인 Http Message Body를 JSON으로 파싱하는 경우 살펴보겠습니다.
아래 그림을 보시죠.

제가 궁금한건 빨간색 부분입니다. 그래서 안에서 어떤 일이 일어나는지 살펴보겠습니다.

일단 MessageConverter 인터페이스 명세를 살펴보겠습니다.

public interface HttpMessageConverter<T> {
    boolean canRead(Class<?> var1, @Nullable MediaType var2);

    boolean canWrite(Class<?> var1, @Nullable MediaType var2);

    List<MediaType> getSupportedMediaTypes();

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

    T read(Class<? extends T> var1, HttpInputMessage var2) throws IOException, HttpMessageNotReadableException;

    void write(T var1, @Nullable MediaType var2, HttpOutputMessage var3) throws IOException, HttpMessageNotWritableException;
}

잘 모르겠지만 readwrite 가능 여부를 체크하는 메서드 canReadcanWrite 메서드가 보입니다. 아마 readRequest 를 읽고 writeResponse 를 쓰는 역할을 하지 않을까 싶습니다.

인터페이스만 봐서는 잘 모르겠네요. 코드를 직접 까보시죠.
위에서 선언해놓은 Controllerreturn 문에 디버그 포인트를 걸고 쭉 들어가보겠습니다.

인텔리제이에서 F7 한참 눌러보니 AbstractHttpMessageConverter 클래스를 찾았고 이 녀석의 canWrite 구현체를 살펴보면 supports(Class<?>) 메서드와 canWrite(MediaType) 메서드를 호출하고 있습니다.

해당 메서드들을 까보기 전에 클래스의 계층을 먼저 살펴보겠습니다.

이렇게 많은 컨버터들이 기본으로 제공되고 있습니다. 가장 마지막에 있는 컨버터들이 구현체입니다. 즉, 실제 코드가 존재합니다. 그 중에서 우리는 JSON 을 담당하는 컨버터를 살펴보겠습니다.

결국 JSON을 직렬화할 때는 appingJackson2HttpMessageConverter 가 사용됩니다.
MappingJackson2MessageConverter의 로직은 결국 if (canWrite()) write(); 로 요약할 수 있습니다.

canWrite() 에서는 class 의 타입을 판단하기 위해서 support() 를 호출하고 MediaType 을 판단하기 위해서 오버로딩 된 canWrite() 를 호출합니다.

그리고 컨버터가 변환할 수 있는 MediaTypesupportedMediaType 과 직렬화되고자 하는 대상의 MediaType 이 같으면 write() 가 실행되는겁니다.

HTTP Message Body 는 여러 MediaType 이 존재합니다. 그리고 그 형태의 정보는 Http Message의 Content-Type 에 저장합니다. 대표적으로 application/json 을 많이 사용하죠. 다 들어보셨죠?

우리가 지금 살펴보고 있는 MappingJackson2HttpMessageConverterapplication/json 에 해당하는 MediaType 일 때 사용됩니다.

생성자를 보면 알 수 있죠. 후훗.. 어쨌든 이녀석 덕분에 우리는 Java Object를 Json으로 직렬화 시킬 수 있는거죠.

MpaiingJack2HttpMessageConverter는 Jackson이라는 objectMapper를 멤버변수로 가지고 있습니다. objectmapper를 사용해서 객체를 JSON으로 바꿀 수 있습니다. 그리고 Spring은 Jackson 이라는 Object Mapper를 기본으로 내장하고 있습니다.

그리고 이 Jackson 이라는 ObjectMapper 를 커스텀해서 Java Object가 Http Body로 직렬화되는 로직을 내 맘대로 바꿀 수 있습니다.

하나 예시를 들어보겠습니다. 우리 dto 를 리턴할 때 생각해볼까요?

@Getter @AllArgsConstructor
class Dto {
	String a;
	int b;
}

위와 같은 dto{ "a" : "string", "b": 1 } 이렇게 JSON으로 변합니다.
getter 가 존재하는 멤버 변수들이 모두 JSON으로 파싱이 된다는 뜻입니다.

그런데 LocalDateTime 은 대표적으로 LocalDate date , LocalTime time 두 멤버변수를 가지고 있지만 getter 가 없습니다.
제가 인텔리제이에서 검색해볼게요.

보세요 IntelliJ도 없다잖아요. 그런데 Spring이 알아서 응답을 내려줍니다.

엥? getter가 없는데 어떻게 내려주는거지? 뭔가 커스텀할 수 있나보다!
저는 김치와 마늘없이는 살 수 없는 코리안이기 때문에 한글로 포맷을 바꿔보겠습니다.

포맷을 바꾸는 방법이 몇 가지 떠오릅니다.

  1. LocalDateTimeVO 로 감싸고 포매팅 메서드를 정의한다.
  2. LocalDateTime 인스턴스에서 값을 죄다 꺼내서 조립해서 리턴한다.
  3. 프론트엔드한테 알아서 파싱하라고 한다.
  4. 우아하게 ObjectMapper를 커스텀한다.

저는 3번이 끌리네요. 우아하게 ObjectMapper를 커스텀합시다.
커스텀하기 위해서 사전 지식이 좀 필요할 것 같습니다.
먼저, AbstractHttpMessageConverter 가 들고 있는 HttpMessageConverter 들입니다.

아무런 설정을 하지 않았지만 아까 클래스 계층에서 봤던 컨버터 구현체들이 존재합니다. (StringHttpMessageConverterMappingJackson2HttpMessageConverter 는 왜 2개인지 모르겠습니다. 아시는 분 댓글좀)

AbstractHttpMessageConverter 는 반복문을 돌며 어떤 구현체로 컨버팅할 것인지 결정합니다.
AbstractHttpMessageConverter.writeWithMessageConverters() 메서드에서 각 컨버터의 canWrite 메서드를 호출하며 그걸 결정합니다.

만약 class 의 타입을 확인하고 (valueType) selectedMediaType 을 확인해서 canWrite()를 호출한 컨버터의 구현체와 일치하면 다음 로직으로 넘어갑니다.

우리는 MappingJackson2HttpMessageConverter 만 보면 되겠죠? 실제로 MappingJackson2HttpMessageConverter 의 차례가 되면 AbstractJackson2HttpMessageConverter.canWrite() 가 호출됩니다.

이제 AbstractJack2HttpMessageConverter 를 살펴 봅시다.

AbstractJackson2HttpMessageConverterdefaultObjectMapper 를 가지고 있는데 기본적으로 들고 있는 ObjectMapper 를 살펴보면 반가운 얼굴이 보입니다.

바로 JavaTimeModule 입니다. JavaTimeModule 이 뭔가 LocalDateTime을 괴롭히고 있을 것 같다는 생각이 드네요. 코드를 까보겠습니다.

찾았다 이짜식. LocalDateTimeSerializer. 이 녀석이 분명 getter 없이도 직렬화를 시켜주고 있을꺼같습니다. 이 녀석도 까봅시다.

코드를 까보니까 이런 로직이네요.
포매터를 등록하지 않았으면 serialize 할 때 defaultFormmatter 를 적용하는데. ISO_LOCAL_DATE_TIME 이라는 기본 포매터라고 합니다. ISO_LOCAL_DATE_TIME 이 뭔지 보시죠.

기본 포매터 즉, ISO_LOCAL_DATE_TIME을 살펴보니 'T' 가 있네요. 얘가 확실합니다.

위의 그림처럼 만든 녀석이 여기있네요. 포매터만 등록해주면 우리가 원하는 형태를 얻을 수 있는거겠죠.
어떻게 해야할까 고민을하다가 LOcalDateTimeSerializer의 생성자 중 좋은 코드를 발견했습니다.
DateTimeFormatter를 넣을 수 있는 생성자!!

아까 if(formatter == null) defaultformatter(); 였으니 생성자에 DateTimeFormatter 를 넣어줍시다. 그러면 더이상 ISO_LOCAL_DATE_TIME이 호출되지 않겠죠?

자. 진정하고 클래스의 계층을 다시 떠올려 봅시다.

우리는 Serializer 를 커스텀하고 싶은거였죠. 그런데 Serializer 만 쏙 빼서 커스텀할 수는 없습니다. 그래서 MappingJackson2HttpMessageConverter부터 싹 다 커스텀 해야 한다는 소리죠.

먼저. LocalDateTimeSerializer 를 커스텀 해보겠습니다.

커스텀 Serializer 를 가지고 있는 Module을 정의한 코드입니다. doYouKnowKimchiFormat을 사용하면 이런 포맷을 가질 수 있습니다.

2021년10월01일19시31분

그리고 LocalDateTimeSerializer 의 생성자를 사용하면 기존의 LocalDateTimeSerializer의 코드들을 모두 사용하면서 포매터만 쏙 바꿔치기 할 수 있습니다.

그리고 이 코드를 ObjectMapper 에 등록해주고 그 ObjectMapperHttpMessageConverter 에 등록해줍시다. 그리고 그 HttpMessageConverter를 스프링 빈으로 등록해주면 되겠죠?

이제 실행시켜봅시다.

Objectmapper 모듈에도 이렇게 LocalDateTimeModule 이 등록되어 있죠.

LocalDateTimeModule 의 포매터에도 잘 들어와 있습니다. 그리고 프론트에서 컨트롤러의 결과 값을 콘솔에 찍어보면?

쟌~ 완성~

예제코드

레퍼런스

HttpMessageConverter
https://bepoz-study-diary.tistory.com/374

커스텀
https://055055.tistory.com/41

커스텀2
https://yoojh9.github.io/스프링부트-HttpMessageConverter/

커스텀3
https://wckhg89.tistory.com/12

로컬데이트타임
https://jsonobject.tistory.com/235

setter가 필요없다.
https://jojoldu.tistory.com/407

디스패처서블릿
https://galid1.tistory.com/525

디스패처서블릿2
https://jeong-pro.tistory.com/225

디스패처서블릿 공식문서
https://docs.spring.io/spring-framework/docs/3.0.0.RC2/spring-framework-reference/html/ch15s02.html

HttpMessageConverter 백기선
https://ict-nroo.tistory.com/98

responseentity 와 advice
https://woodcock.tistory.com/19

스프링 MVC 1편 김영한
https://www.inflearn.com/course/스프링-mvc-1/lecture/71225?tab=curriculum&speed=1.5

profile
노력하는 자는 즐기는 자를 이길 수 없다 를 알면서도 게으름에 지는 중

0개의 댓글