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 대안) (폼/멀티파트)
인터페이스는 단순하다(소스: 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 — 한 번만 읽을 수 있는 페이로드 대비).
대부분의 구현은 이 추상 베이스를 상속한다(소스: http/converter/AbstractHttpMessageConverter.java). 두 가지를 표준화한다.
- 미디어 타입 매칭 —
setSupportedMediaTypes(...)로 지원 타입을 등록하면,canRead는supports(clazz) && 지원타입.includes(mediaType),canWrite는supports(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;
}- 공통 헤더 처리 —
write는final이며, 내부적으로addDefaultHeaders(Content-Type/Content-Length 설정)를 호출한 뒤 하위 클래스의writeInternal에 위임한다. 읽기도read→readInternal템플릿 메서드 패턴이다. 변환기 작성자는 "기본 헤더는 어떻게 채울지" 고민할 필요 없이 실제 직렬화만 구현한다.
String이나 byte[]는 Class만 있으면 충분하지만, List<User>는 Class가 List라서 원소 타입을 잃는다. JSON 직렬화는 이 정보가 필수다. 그래서 두 확장이 있다.
GenericHttpMessageConverter<T>—read(Type, Class, ...)/write(T, Type, ...)처럼java.lang.reflect.Type을 받아 제네릭을 보존한다. JSON/XML 변환기가 구현.SmartHttpMessageConverter<T>(7.0) —ResolvableType과 write 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 혼합 멀티파트를 만든다.
전통적으로 변환기 목록은 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 설정에서도 공유된다.
서버가 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) →addDefaultHeaders→writeInternal— 헤더 처리와 직렬화를 분리한 템플릿 메서드. 하위 클래스 부담을 줄인다.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장)가 공유하는 공통 엔진이다.