스프링을 다루면서 이런 궁금증이 있었습니다.
리턴 타입이 다른데 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;
}
잘 모르겠지만 read
와 write
가능 여부를 체크하는 메서드 canRead
와 canWrite
메서드가 보입니다. 아마 read
가 Request
를 읽고 write
가 Response
를 쓰는 역할을 하지 않을까 싶습니다.
인터페이스만 봐서는 잘 모르겠네요. 코드를 직접 까보시죠.
위에서 선언해놓은 Controller
의 return
문에 디버그 포인트를 걸고 쭉 들어가보겠습니다.
인텔리제이에서 F7
한참 눌러보니 AbstractHttpMessageConverter
클래스를 찾았고 이 녀석의 canWrite
구현체를 살펴보면 supports(Class<?>)
메서드와 canWrite(MediaType)
메서드를 호출하고 있습니다.
해당 메서드들을 까보기 전에 클래스의 계층을 먼저 살펴보겠습니다.
이렇게 많은 컨버터들이 기본으로 제공되고 있습니다. 가장 마지막에 있는 컨버터들이 구현체입니다. 즉, 실제 코드가 존재합니다. 그 중에서 우리는 JSON
을 담당하는 컨버터를 살펴보겠습니다.
결국 JSON
을 직렬화할 때는 appingJackson2HttpMessageConverter
가 사용됩니다.
MappingJackson2MessageConverter
의 로직은 결국 if (canWrite()) write();
로 요약할 수 있습니다.
canWrite()
에서는 class
의 타입을 판단하기 위해서 support()
를 호출하고 MediaType
을 판단하기 위해서 오버로딩 된 canWrite()
를 호출합니다.
그리고 컨버터가 변환할 수 있는 MediaType
인 supportedMediaType
과 직렬화되고자 하는 대상의 MediaType
이 같으면 write()
가 실행되는겁니다.
HTTP Message Body 는 여러 MediaType
이 존재합니다. 그리고 그 형태의 정보는 Http Message의 Content-Type
에 저장합니다. 대표적으로 application/json
을 많이 사용하죠. 다 들어보셨죠?
우리가 지금 살펴보고 있는 MappingJackson2HttpMessageConverter
는 application/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
가 없는데 어떻게 내려주는거지? 뭔가 커스텀할 수 있나보다!
저는 김치와 마늘없이는 살 수 없는 코리안이기 때문에 한글로 포맷을 바꿔보겠습니다.
포맷을 바꾸는 방법이 몇 가지 떠오릅니다.
LocalDateTime
을 VO
로 감싸고 포매팅 메서드를 정의한다.LocalDateTime
인스턴스에서 값을 죄다 꺼내서 조립해서 리턴한다.저는 3번이 끌리네요. 우아하게 ObjectMapper를 커스텀합시다.
커스텀하기 위해서 사전 지식이 좀 필요할 것 같습니다.
먼저, AbstractHttpMessageConverter
가 들고 있는 HttpMessageConverter
들입니다.
아무런 설정을 하지 않았지만 아까 클래스 계층에서 봤던 컨버터 구현체들이 존재합니다. (StringHttpMessageConverter
와 MappingJackson2HttpMessageConverter
는 왜 2개인지 모르겠습니다. 아시는 분 댓글좀)
AbstractHttpMessageConverter
는 반복문을 돌며 어떤 구현체로 컨버팅할 것인지 결정합니다.
AbstractHttpMessageConverter.writeWithMessageConverters()
메서드에서 각 컨버터의 canWrite
메서드를 호출하며 그걸 결정합니다.
만약 class
의 타입을 확인하고 (valueType
) selectedMediaType
을 확인해서 canWrite()
를 호출한 컨버터의 구현체와 일치하면 다음 로직으로 넘어갑니다.
우리는 MappingJackson2HttpMessageConverter
만 보면 되겠죠? 실제로 MappingJackson2HttpMessageConverter
의 차례가 되면 AbstractJackson2HttpMessageConverter.canWrite()
가 호출됩니다.
이제 AbstractJack2HttpMessageConverter
를 살펴 봅시다.
AbstractJackson2HttpMessageConverter
는 defaultObjectMapper
를 가지고 있는데 기본적으로 들고 있는 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
에 등록해주고 그 ObjectMapper
를 HttpMessageConverter
에 등록해줍시다. 그리고 그 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