HttpMessageConverter란?
Spring MVC에서 HTTP 요청 본문을 Java 객체로 변환하고(@RequestBody), Java 객체를 HTTP 응답 본문으로 직렬화(@ResponseBody)하는 핵심 인터페이스가 HttpMessageConverter입니다. Content-Type과 Accept 헤더를 기반으로 적절한 컨버터를 자동 선택합니다.
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, MediaType mediaType);
boolean canWrite(Class<?> clazz, MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage);
void write(T t, MediaType contentType, HttpOutputMessage outputMessage);
}
Spring Boot는 클래스패스에 따라 Jackson, Gson, JAXB 등의 컨버터를 자동 등록합니다. 하지만 CSV 내보내기, Protocol Buffers, 커스텀 바이너리 포맷 등은 직접 구현해야 합니다.
기본 등록 컨버터 동작 순서
Spring Boot가 자동 등록하는 컨버터들과 우선순위를 이해해야 예상치 못한 직렬화 문제를 디버깅할 수 있습니다.
| 컨버터 | MediaType | 용도 |
|---|---|---|
ByteArrayHttpMessageConverter |
*/* | byte[] 처리 |
StringHttpMessageConverter |
text/plain | String 처리 (UTF-8) |
ResourceHttpMessageConverter |
*/* | Resource (파일 다운로드) |
MappingJackson2HttpMessageConverter |
application/json | JSON 직렬화/역직렬화 |
Jaxb2RootElementHttpMessageConverter |
application/xml | XML (JAXB 존재 시) |
컨버터는 등록 순서대로 canRead()/canWrite()를 호출하여 첫 번째로 매칭되는 컨버터를 사용합니다. 커스텀 컨버터의 우선순위가 중요한 이유입니다.
실전: CSV HttpMessageConverter 구현
엑셀 대신 CSV 내보내기 API를 만들 때, 커스텀 컨버터로 깔끔하게 구현할 수 있습니다.
@Component
public class CsvHttpMessageConverter<T>
extends AbstractHttpMessageConverter<List<T>> {
private static final MediaType TEXT_CSV =
new MediaType("text", "csv", StandardCharsets.UTF_8);
public CsvHttpMessageConverter() {
super(TEXT_CSV);
}
@Override
protected boolean supports(Class<?> clazz) {
return List.class.isAssignableFrom(clazz);
}
@Override
protected List<T> readInternal(
Class<? extends List<T>> clazz,
HttpInputMessage inputMessage) {
throw new UnsupportedOperationException("CSV 읽기 미지원");
}
@Override
protected void writeInternal(
List<T> data,
HttpOutputMessage outputMessage) throws IOException {
outputMessage.getHeaders().setContentType(TEXT_CSV);
outputMessage.getHeaders().set(
"Content-Disposition",
"attachment; filename="export.csv""
);
try (var writer = new OutputStreamWriter(
outputMessage.getBody(), StandardCharsets.UTF_8)) {
if (data.isEmpty()) return;
// 헤더 추출 (리플렉션)
Field[] fields = data.get(0).getClass().getDeclaredFields();
String header = Arrays.stream(fields)
.map(Field::getName)
.collect(Collectors.joining(","));
writer.write(header + "n");
// 데이터 행
for (T item : data) {
String row = Arrays.stream(fields)
.peek(f -> f.setAccessible(true))
.map(f -> {
try {
Object val = f.get(item);
return val != null ? escapeCsv(val.toString()) : "";
} catch (IllegalAccessException e) {
return "";
}
})
.collect(Collectors.joining(","));
writer.write(row + "n");
}
}
}
private String escapeCsv(String value) {
if (value.contains(",") || value.contains(""") || value.contains("n")) {
return """ + value.replace(""", """") + """;
}
return value;
}
}
컨트롤러에서는 produces로 MediaType만 지정하면 자동으로 CSV 컨버터가 선택됩니다.
@GetMapping(value = "/users/export", produces = "text/csv")
public List<UserDto> exportUsers() {
return userService.findAll();
}
// Accept: text/csv → CsvHttpMessageConverter 자동 선택
Content Negotiation 전략
같은 엔드포인트에서 JSON과 CSV를 모두 지원하려면 Content Negotiation을 활용합니다.
@GetMapping(value = "/users",
produces = { "application/json", "text/csv" })
public List<UserDto> getUsers() {
return userService.findAll();
}
// Accept: application/json → Jackson 컨버터
// Accept: text/csv → CSV 컨버터
URL 확장자 기반 협상도 가능합니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(
ContentNegotiationConfigurer configurer) {
configurer
.favorParameter(true)
.parameterName("format")
.defaultContentType(MediaType.APPLICATION_JSON)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("csv", MediaType.valueOf("text/csv"))
.mediaType("xml", MediaType.APPLICATION_XML);
}
}
// GET /users?format=csv → CSV 응답
// GET /users?format=json → JSON 응답
Protocol Buffers 컨버터
마이크로서비스 간 통신에서 Protobuf는 JSON 대비 직렬화 크기 3~10배 감소, 파싱 속도 5~100배 향상을 제공합니다. Spring은 ProtobufHttpMessageConverter를 기본 제공합니다.
// build.gradle
implementation 'com.google.protobuf:protobuf-java:3.25.0'
implementation 'com.google.protobuf:protobuf-java-util:3.25.0'
// 설정
@Configuration
public class ProtobufConfig {
@Bean
public ProtobufHttpMessageConverter protobufConverter() {
return new ProtobufHttpMessageConverter();
}
}
// 컨트롤러
@GetMapping(value = "/users/{id}",
produces = { "application/x-protobuf", "application/json" })
public UserProto.User getUser(@PathVariable Long id) {
User user = userService.findById(id);
return UserProto.User.newBuilder()
.setId(user.getId())
.setName(user.getName())
.setEmail(user.getEmail())
.build();
}
클라이언트가 Accept: application/x-protobuf로 요청하면 바이너리 Protobuf로, Accept: application/json이면 JSON으로 응답합니다. 동일 엔드포인트에서 두 포맷을 모두 지원하는 패턴입니다.
컨버터 등록과 우선순위 제어
커스텀 컨버터의 등록 위치에 따라 동작이 달라집니다.
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 방법 1: 기존 컨버터 뒤에 추가
@Override
public void extendMessageConverters(
List<HttpMessageConverter<?>> converters) {
converters.add(new CsvHttpMessageConverter<>());
}
// 방법 2: 기존 컨버터 앞에 추가 (우선순위 높음)
@Override
public void extendMessageConverters(
List<HttpMessageConverter<?>> converters) {
converters.add(0, new CsvHttpMessageConverter<>());
}
// 방법 3: 기존 컨버터 전부 교체 (주의!)
@Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter());
converters.add(new CsvHttpMessageConverter<>());
// 기본 컨버터가 모두 사라짐!
}
}
- extendMessageConverters: 기본 컨버터 유지 + 추가 (권장)
- configureMessageConverters: 기본 컨버터 제거 후 수동 등록 (세밀한 제어 필요 시)
- 인덱스 0에 추가하면
canWrite()에서 먼저 매칭되어 우선순위가 높아짐
디버깅: 406 Not Acceptable 해결
클라이언트의 Accept 헤더와 매칭되는 컨버터가 없으면 406 에러가 발생합니다. 디버깅 방법:
// 현재 등록된 컨버터 목록 확인
@RestController
public class DebugController {
@Autowired
private RequestMappingHandlerAdapter adapter;
@GetMapping("/debug/converters")
public List<String> listConverters() {
return adapter.getMessageConverters().stream()
.map(c -> c.getClass().getSimpleName()
+ " → " + c.getSupportedMediaTypes())
.toList();
}
}
// 로그 레벨 조정
// application.yml
logging:
level:
org.springframework.web.servlet.mvc.method.annotation: DEBUG
org.springframework.http.converter: TRACE
TRACE 로그에서 “Writing […]” 또는 “No converter found” 메시지로 어떤 컨버터가 선택(또는 실패)되었는지 확인할 수 있습니다.
관련 글
- Spring Jackson 직렬화 심화 — Jackson 기반 JSON 컨버터의 커스터마이징 전략
- Spring ArgumentResolver 심화 — 요청 파라미터 변환과 HttpMessageConverter의 차이점