Skip to content

Latest commit

 

History

History
157 lines (121 loc) · 12.1 KB

File metadata and controls

157 lines (121 loc) · 12.1 KB

02. 메시지 변환 — HttpMessageConverter

무엇을 / 왜

HTTP 바디는 바이트 스트림이지만, 애플리케이션은 User·List<Order>·MultiValueMap 같은 자바 객체로 일한다. 그 사이를 누가, 어떤 규칙으로 변환하는가? 이걸 한 곳에 모으지 않으면 컨트롤러마다 JSON 직렬화 코드가 흩어지고, 서버와 클라이언트가 서로 다른 규칙을 쓰게 된다.

HttpMessageConverter는 그 "변환 규칙"을 캡슐화한 전략(Strategy) 인터페이스다. @RequestBody/@ResponseBody, ResponseEntity, RestClient/RestTemplate가 전부 이 전략 목록을 공유한다. 이 장은 변환기 계층의 구조와, 7.0에서 도입된 HttpMessageConverters 레지스트리를 다룬다. (블로킹/명령형 변환. 리액티브 codec은 06장.)

핵심 타입

                ┌─────────────────────────────┐
                │   HttpMessageConverter<T>     │
                │  canRead / canWrite           │
                │  read(class, HttpInputMessage) │
                │  write(t, mediaType, HttpOutputMessage)
                └──────────────┬──────────────┘
        ┌───────────────────────┼───────────────────────────┐
        ▼                       ▼                            ▼
┌────────────────┐  ┌──────────────────────────┐  ┌──────────────────────┐
│ Abstract        │  │ GenericHttpMessageConverter│  │ SmartHttpMessageConv. │
│ HttpMessage     │  │  + read/write(Type ...)   │  │  + ResolvableType,     │
│ Converter<T>    │  │  (제네릭 타입 보존)         │  │    write hints (7.0)   │
└──────┬─────────┘  └───────────┬──────────────┘  └──────────────────────┘
       │                        │
       │            ┌───────────┴───────────────┐
       ▼            ▼                            ▼
 String / ByteArray  AbstractGenericHttp     AbstractJackson
 / Resource /        MessageConverter        HttpMessageConverter
 BufferedImage 등    (json/xml 공통 베이스)   (Jackson 3.x 통합)
                          │
              ┌───────────┼─────────────┬──────────────┐
              ▼           ▼             ▼              ▼
     MappingJackson  Jaxb2Root   GsonHttp/Jsonb   FormHttp
     (legacy json)   (xml)       (json 대안)      (폼/멀티파트)

HttpMessageConverter — 네 개의 질문과 두 개의 동작

인터페이스는 단순하다(소스: http/converter/HttpMessageConverter.java).

boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
T    read(Class<? extends T> clazz, HttpInputMessage inputMessage);
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage);

canRead/canWrite는 "이 변환기가 이 타입을 이 미디어 타입으로 처리할 수 있는가"를 묻는 선택 질의다. 프레임워크는 변환기 목록을 순회하며 처음 true를 반환하는 변환기를 쓴다. read/write는 실제 변환이다. 입출력이 01장의 HttpInputMessage/HttpOutputMessage라는 점에 주목 — 그래서 서버와 클라이언트가 같은 변환기를 공유한다.

7.1에서 canWriteRepeatedly(t, contentType) 기본 메서드가 추가됐다. HTTP 리다이렉트 후 같은 바디를 다시 보낼 수 있는지를 클라이언트가 알 수 있게 한다(기본 false — 한 번만 읽을 수 있는 페이로드 대비).

AbstractHttpMessageConverter — 공통 골격

대부분의 구현은 이 추상 베이스를 상속한다(소스: http/converter/AbstractHttpMessageConverter.java). 두 가지를 표준화한다.

  1. 미디어 타입 매칭setSupportedMediaTypes(...)로 지원 타입을 등록하면, canReadsupports(clazz) && 지원타입.includes(mediaType), canWritesupports(clazz) && 지원타입.isCompatibleWith(mediaType)로 자동 판정한다. 하위 클래스는 supports(Class)만 구현하면 된다.
@Override
public boolean canRead(Class<?> clazz, @Nullable MediaType mediaType) {
    return supports(clazz) && canRead(mediaType);
}
protected boolean canRead(@Nullable MediaType mediaType) {
    if (mediaType == null) return true;
    for (MediaType supported : getSupportedMediaTypes())
        if (supported.includes(mediaType)) return true;
    return false;
}
  1. 공통 헤더 처리writefinal이며, 내부적으로 addDefaultHeaders(Content-Type/Content-Length 설정)를 호출한 뒤 하위 클래스의 writeInternal에 위임한다. 읽기도 readreadInternal 템플릿 메서드 패턴이다. 변환기 작성자는 "기본 헤더는 어떻게 채울지" 고민할 필요 없이 실제 직렬화만 구현한다.

Generic / Smart — 제네릭 타입 정보가 필요할 때

String이나 byte[]Class만 있으면 충분하지만, List<User>ClassList라서 원소 타입을 잃는다. JSON 직렬화는 이 정보가 필수다. 그래서 두 확장이 있다.

  • GenericHttpMessageConverter<T>read(Type, Class, ...)/write(T, Type, ...)처럼 java.lang.reflect.Type을 받아 제네릭을 보존한다. JSON/XML 변환기가 구현.
  • SmartHttpMessageConverter<T>(7.0) — ResolvableTypewrite hints(Map<String,Object>)를 받는 더 풍부한 형태. Jackson 3.x 통합(AbstractJacksonHttpMessageConverter)의 기반이며, JSON View 같은 힌트를 전달한다.

구체 변환기들

변환기 처리 타입 미디어 타입
StringHttpMessageConverter String text/plain 등 (charset 처리)
ByteArrayHttpMessageConverter byte[] application/octet-stream
ResourceHttpMessageConverter Resource 전 타입(스트리밍 파일 전송)
ResourceRegionHttpMessageConverter ResourceRegion Range 부분 전송
FormHttpMessageConverter MultiValueMap<String,?> application/x-www-form-urlencoded, multipart/form-data
AbstractJacksonHttpMessageConverter 계열 임의 객체 application/json 등 (Jackson 3)
MappingJackson2HttpMessageConverter 임의 객체 JSON (Jackson 2.x, 레거시)
Jaxb2RootElementHttpMessageConverter @XmlRootElement application/xml
KotlinSerialization* @Serializable JSON/CBOR/ProtoBuf
ObjectToStringHttpMessageConverter 임의 객체 ConversionService 경유 문자열화

FormHttpMessageConverter는 특히 흥미롭다. multipart/form-data 쓰기 시 각 파트를 다시 다른 변환기들에 위임해(중첩 변환) 파일+JSON 혼합 멀티파트를 만든다.

HttpMessageConverters — 7.0의 변환기 레지스트리

전통적으로 변환기 목록RestTemplate/MVC 설정이 각자 List<HttpMessageConverter>로 들고 있었다. 7.0은 이를 HttpMessageConverters라는 1급 레지스트리로 승격했다(소스: http/converter/HttpMessageConverters.java).

HttpMessageConverters converters = HttpMessageConverters.forClient()
        .registerDefaults()                 // classpath 감지로 기본 변환기 등록
        .withJsonConverter(myJsonConverter) // 특정 변환기만 교체
        .build();
  • forClient() / forServer() — 클라이언트용·서버용으로 순서가 다른 기본 목록을 만든다(소스 주석에 순서 명시). 예컨대 서버 빌더는 ResourceRegionHttpMessageConverter를 포함(부분 전송 응답)하지만 클라이언트는 다르다.
  • registerDefaults() — Jackson/Gson/JSON-B, XML, CBOR, YAML, Smile, ProtoBuf, Kotlin Serialization 등을 classpath에 있을 때만 자동 등록. "있으면 켜지는" 런타임 감지가 여기 집약된다.
  • Iterable<HttpMessageConverter<?>>를 구현해, 만들어진 결과는 불변 목록처럼 순회된다.

이 레지스트리는 RestClient.Builder.messageConverters(...)로 클라이언트에 주입되고(03장), MVC/WebFlux 설정에서도 공유된다.

동작 흐름 — @RequestBody 역직렬화

서버가 JSON 요청 바디를 User로 바꾸는 대표 경로다.

요청: POST /users  Content-Type: application/json   { "name": "kim" }
        │
        ▼  프레임워크가 변환기 목록 순회
┌────────────────────────────────────────────┐
│ for (converter : converters)                 │
│   if (converter.canRead(User.class,          │  ← MediaType.includes(json)
│                         APPLICATION_JSON)) {  │     + supports(User)
│      return converter.read(User.class,        │
│                            inputMessage);     │  ← HttpInputMessage.getBody()
│   }                                           │     = 요청 InputStream
└────────────────────────────────────────────┘
        │  JacksonJsonHttpMessageConverter 선택
        ▼
   Jackson ObjectMapper로 InputStream → User 역직렬화
        │  실패 시
        ▼
   HttpMessageNotReadableException (→ 400으로 매핑)

쓰기(@ResponseBody/ResponseEntity)는 정확히 대칭이다. canWrite로 변환기를 고르고, Accept 헤더와 협상된 MediaType(04장)을 write에 넘겨 HttpOutputMessage.getBody()로 직렬화한다. 변환 실패는 HttpMessageNotWritableException이다.

핵심 메서드

  • canRead/canWrite — 변환기 선택의 게이트. 프레임워크는 목록 순서대로 첫 매칭을 쓰므로, 변환기 등록 순서가 곧 우선순위다.
  • AbstractHttpMessageConverter.write(final) → addDefaultHeaderswriteInternal — 헤더 처리와 직렬화를 분리한 템플릿 메서드. 하위 클래스 부담을 줄인다.
  • HttpMessageConverters.forClient()/forServer().registerDefaults() — "있으면 켜지는" 변환기 자동 구성의 단일 지점.
  • GenericHttpMessageConverter.read(Type, ...) — 제네릭 컬렉션 역직렬화를 가능케 하는 오버로드.

설계 포인트 / 확장점

  • 전략 + 우선순위 목록: 변환기는 자기가 처리할 수 있는지만 답하고, 선택은 프레임워크가 순서로 결정한다. 새 포맷 지원은 변환기 하나를 목록 앞에 추가하면 끝.
  • 템플릿 메서드: Abstract*Converter가 헤더·미디어타입 매칭을 표준화해, 실제 구현은 supports/readInternal/writeInternal만 채운다.
  • 서버/클라이언트 비대칭 인지: forServer()/forClient()로 기본 목록을 분리해, 서버에만 필요한 변환기(부분 전송 등)와 클라이언트 기본을 구분한다.
  • 확장점: 커스텀 HttpMessageConverter를 구현하거나, HttpMessageConverters.Builder로 특정 변환기만 교체/추가하면 전 영역(MVC·클라이언트)에 일괄 반영된다.

정리

HttpMessageConverter는 객체 ↔ HTTP 바디 변환을 캡슐화한 전략이며, canRead/canWrite라는 선택 질의와 read/write 동작으로 구성된다. AbstractHttpMessageConverter가 미디어 타입 매칭과 헤더 처리를 템플릿화하고, Generic/Smart 변형이 제네릭·힌트가 필요한 JSON/XML을 떠받친다. 7.0의 HttpMessageConverters 레지스트리는 classpath 감지 기반 기본 구성을 서버/클라이언트용으로 묶어 한 곳에서 관리하게 만든다. 이 변환 전략은 서버의 @RequestBody/ResponseEntity와 클라이언트의 RestClient(03장)가 공유하는 공통 엔진이다.