HttpMessageConverter — JSON 요청·응답은 어떻게 변환될까
컨트롤러에서 자바 객체를 반환하면 클라이언트에게 JSON이 도착합니다. 이 변환은 누가, 어떤 기준으로 하는 걸까요? 그리고 이 과정을 커스터마이징할 수 있을까요?
개념 정의
HttpMessageConverter 는 HTTP 요청 본문을 자바 객체로 변환(역직렬화)하거나, 자바 객체를 HTTP 응답 본문으로 변환(직렬화)하는 스프링의 인터페이스입니다. REST API에서 JSON ↔ 객체 변환의 핵심입니다.
왜 필요한가
HTTP는 텍스트 기반 프로토콜이고, 자바 애플리케이션은 객체를 다룹니다. 이 둘 사이의 변환을 자동으로 처리해줘야 개발자가 직렬화/역직렬화 코드를 반복 작성하지 않아도 됩니다.
// HttpMessageConverter가 없다면...
@PostMapping("/users")
public void createUser(HttpServletRequest request, HttpServletResponse response) throws IOException {
String json = new BufferedReader(request.getReader()).lines().collect(Collectors.joining());
ObjectMapper mapper = new ObjectMapper();
CreateUserRequest dto = mapper.readValue(json, CreateUserRequest.class);
User user = userService.create(dto);
String responseJson = mapper.writeValueAsString(user);
response.setContentType("application/json");
response.getWriter().write(responseJson);
}
HttpMessageConverter가 있으면 이 모든 것이 자동입니다.
내부 동작
컨버터 선택 과정
요청 시 (역직렬화):
1. @RequestBody 파라미터 감지
2. 요청의 Content-Type 헤더 확인 (예: application/json)
3. 등록된 HttpMessageConverter 목록을 순회
4. canRead(targetType, contentType) == true인 컨버터 선택
5. 선택된 컨버터의 read() 호출 → 자바 객체 반환
** 응답 시 (직렬화)**:
1. @ResponseBody 또는 @RestController 감지
2. 클라이언트의 Accept 헤더 확인
3. 등록된 HttpMessageConverter 목록을 순회
4. canWrite(returnType, acceptType) == true인 컨버터 선택
5. 선택된 컨버터의 write() 호출 → HTTP 응답 본문 작성
기본 등록 컨버터 (순서대로)
1. ByteArrayHttpMessageConverter → byte[]
2. StringHttpMessageConverter → String
3. ResourceHttpMessageConverter → Resource
4. MappingJackson2HttpMessageConverter → JSON ↔ Object
5. MappingJackson2XmlHttpMessageConverter → XML ↔ Object (Jackson XML 모듈)
6. FormHttpMessageConverter → form data
Jackson이 클래스패스에 있으면 MappingJackson2HttpMessageConverter가 자동 등록됩니다.
코드 예제
ObjectMapper 커스터마이징
@Configuration
public class JacksonConfig {
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
// 알 수 없는 필드 무시
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
// 빈 객체 직렬화 허용
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
// 날짜를 타임스탬프 대신 ISO-8601로
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
// null 필드 제외
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
// Java 8 날짜 모듈 등록
.registerModule(new JavaTimeModule())
// 들여쓰기 (개발 환경)
.enable(SerializationFeature.INDENT_OUTPUT);
}
}
application.yml로 Jackson 설정
spring:
jackson:
default-property-inclusion: non_null # null 필드 제외
deserialization:
fail-on-unknown-properties: false # 알 수 없는 필드 무시
serialization:
write-dates-as-timestamps: false # ISO-8601 날짜 형식
indent-output: true # 들여쓰기 (개발용)
date-format: yyyy-MM-dd HH:mm:ss # 날짜 포맷
time-zone: Asia/Seoul # 타임존
Jackson 어노테이션 활용
public class UserResponse {
private Long id;
@JsonProperty("user_name") // JSON 필드명 변경
private String name;
@JsonIgnore // 직렬화/역직렬화에서 제외
private String password;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") // 날짜 포맷
private LocalDateTime createdAt;
@JsonInclude(JsonInclude.Include.NON_EMPTY) // 빈 값이면 제외
private List<String> roles;
}
@JsonView — API마다 다른 필드 노출
public class Views {
public interface Summary {}
public interface Detail extends Summary {}
}
public class UserResponse {
@JsonView(Views.Summary.class)
private Long id;
@JsonView(Views.Summary.class)
private String name;
@JsonView(Views.Detail.class) // Detail에서만 노출
private String email;
이어서 @JsonView을 적용한 나머지 구현부입니다.
@JsonView(Views.Detail.class)
private LocalDateTime createdAt;
}
@RestController
public class UserController {
@GetMapping("/users")
@JsonView(Views.Summary.class) // id, name만 반환
public List<UserResponse> list() { ... }
@GetMapping("/users/{id}")
@JsonView(Views.Detail.class) // 모든 필드 반환
public UserResponse detail(@PathVariable Long id) { ... }
}
커스텀 HttpMessageConverter
CSV 형식을 처리하는 커스텀 컨버터 예시입니다.
public class CsvHttpMessageConverter extends AbstractHttpMessageConverter<List<String[]>> {
public CsvHttpMessageConverter() {
super(new MediaType("text", "csv"));
}
@Override
protected boolean supports(Class<?> clazz) {
return List.class.isAssignableFrom(clazz);
}
이어서 나머지 어노테이션 기반 구현부입니다.
@Override
protected List<String[]> readInternal(Class<? extends List<String[]>> clazz,
HttpInputMessage inputMessage) throws IOException {
// CSV 파싱 로직
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(inputMessage.getBody()))) {
return reader.lines()
.map(line -> line.split(","))
.collect(Collectors.toList());
}
}
이어서 @Override을 적용한 나머지 구현부입니다.
@Override
protected void writeInternal(List<String[]> data,
HttpOutputMessage outputMessage) throws IOException {
// CSV 쓰기 로직
try (OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody())) {
for (String[] row : data) {
writer.write(String.join(",", row) + "\n");
}
}
}
}
// 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CsvHttpMessageConverter());
}
}
컨버터 추가 vs 교체
@Configuration
public class WebConfig implements WebMvcConfigurer {
// 기존 컨버터에 추가 (기본 컨버터 유지)
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CsvHttpMessageConverter());
}
// 컨버터 목록 완전 교체 (기본 컨버터 제거됨 — 주의!)
// @Override
// public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// converters.add(new MappingJackson2HttpMessageConverter());
// converters.add(new CsvHttpMessageConverter());
// }
}
configureMessageConverters를 사용하면 기본 컨버터가 모두 사라지므로 주의해야 합니다. 대부분의 경우 extendMessageConverters를 사용합니다.
직렬화/역직렬화 디버깅
logging:
level:
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor: DEBUG
org.springframework.http.converter: DEBUG
주의할 점
1. 기본 생성자가 없는 DTO는 Jackson 역직렬화에 실패한다
Jackson은 기본적으로 기본 생성자(no-arg constructor)로 객체를 생성한 뒤 setter나 리플렉션으로 값을 채웁니다. @AllArgsConstructor만 있고 기본 생성자가 없으면 InvalidDefinitionException이 발생합니다. @NoArgsConstructor를 추가하거나 @JsonCreator로 생성자를 지정해야 합니다.
2. 순환 참조가 있는 엔티티를 직접 반환하면 StackOverflowError가 발생한다
JPA 엔티티에서 Order가 User를 참조하고 User가 Order 목록을 참조하면, Jackson이 직렬화할 때 무한 재귀가 발생하여 StackOverflowError가 터집니다. 엔티티를 직접 반환하지 말고 DTO로 변환하거나, @JsonIgnore/@JsonManagedReference로 순환을 끊어야 합니다.
3. Content-Type 헤더가 맞지 않으면 415 Unsupported Media Type이 발생한다
클라이언트가 Content-Type: text/plain으로 JSON을 보내면 MappingJackson2HttpMessageConverter가 매칭되지 않아 415 Unsupported Media Type이 반환됩니다. API 클라이언트에서 Content-Type: application/json 헤더를 빠뜨리는 실수가 잦으며, 에러 메시지만 봐서는 원인을 파악하기 어렵습니다.
정리
- HttpMessageConverter 는 HTTP 본문 ↔ 자바 객체 변환을 담당합니다
- JSON 변환은 기본적으로 Jackson(ObjectMapper) 이 처리합니다
application.yml이나@Bean ObjectMapper로 Jackson 동작을 커스터마이징할 수 있습니다@JsonView로 같은 DTO에서 API마다 다른 필드를 노출할 수 있습니다- 커스텀 미디어 타입이 필요하면
AbstractHttpMessageConverter를 구현합니다 - 컨버터 추가 시
extendMessageConverters를 사용해 기본 컨버터를 유지합니다