HTTP 요청/응답의 Body 데이터를 Java 객체로 변환하거나, 그 반대로 변환해주는 Spring의 핵심 컴포넌트
우체부가 편지를 배달할 때, 한국어 편지를 영어로 번역하거나 영어 편지를 한국어로 번역해주는 번역가 역할
클라이언트 ↔ [HttpMessageConverter] ↔ Spring Controller
JSON ↔ [번역가] ↔ Java 객체
// ❌ 이런 식으로는 할 수 없음
@PostMapping("/users")
public String createUser(String jsonString) {
// JSON 문자열을 어떻게 User 객체로 바꿀까? 😵
// "{\"name\":\"김개발\", \"age\":25}" → User 객체
}
// ✅ 자동으로 JSON → User 객체 변환!
@PostMapping("/users")
public User createUser(@RequestBody User user) {
// 와! JSON이 자동으로 User 객체가 되었다! 🎉
return user; // User 객체 → JSON 자동 변환!
}
1. 클라이언트가 JSON 전송
POST /users
Content-Type: application/json
{"name": "김개발", "age": 25}
2. HttpMessageConverter가 변환
JSON → User 객체
3. Controller에서 User 객체 사용
user.getName() // "김개발"
user.getAge() // 25
4. 응답시 다시 변환
User 객체 → JSON
HTTP 요청
↓
DispatcherServlet (Spring MVC의 중앙 관제탑)
↓
HandlerAdapter (Controller 호출 담당)
↓
ArgumentResolver (@RequestBody 처리 담당)
↓
HttpMessageConverter (실제 변환 작업)
↓
Controller 메서드 실행
↓
ReturnValueHandler (응답 처리 담당)
↓
HttpMessageConverter (응답 변환)
↓
HTTP 응답
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)
throws IOException, HttpMessageNotReadableException;
void write(T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
| 순위 | Converter | 처리 대상 | 요청 타입 | 응답 타입 | 용도 |
|---|---|---|---|---|---|
| 1 | ByteArrayHttpMessageConverter | byte[] | / | application/octet-stream | 파일, 이미지 |
| 2 | StringHttpMessageConverter | String | / | text/plain | 단순 텍스트 |
| 3 | ResourceHttpMessageConverter | Resource | / | - | 정적 리소스 |
| 4 | FormHttpMessageConverter | MultiValueMap | application/x-www-form-urlencoded | - | 폼 데이터 |
| 5 | MappingJackson2HttpMessageConverter | Object | application/json | application/json | JSON (가장 중요!) |
// 요청 데이터
POST /api/users
Content-Type: application/json
{"name": "김개발", "age": 25}
// Controller
@PostMapping("/api/users")
public User createUser(@RequestBody User user) {
System.out.println(user.getName()); // "김개발"
return user;
}
// 동작 과정
1. ByteArrayHttpMessageConverter.canRead() → false (byte[]가 아님)
2. StringHttpMessageConverter.canRead() → false (String이 아님)
3. MappingJackson2HttpMessageConverter.canRead() → true! ✅
4. JSON을 User 객체로 변환 완료!
// 같은 요청이지만 String으로 받기
@PostMapping("/api/string")
public String handleJsonAsString(@RequestBody String jsonData) {
System.out.println(jsonData); // "{\"name\":\"김개발\",\"age\":25}"
return "받았습니다!";
}
// 동작 과정
1. ByteArrayHttpMessageConverter.canRead() → false
2. StringHttpMessageConverter.canRead() → true! ✅
3. JSON을 String으로 변환 (우선순위 때문에 Jackson보다 먼저)
// 요청 데이터
POST /api/users
Content-Type: text/plain
김개발
// Controller
@PostMapping("/api/users")
public User createUser(@RequestBody User user) {
return user;
}
// 동작 과정
1. ByteArrayHttpMessageConverter.canRead() → false
2. StringHttpMessageConverter.canRead() → false (User 객체가 아님)
3. MappingJackson2HttpMessageConverter.canRead() → false (JSON이 아님)
4. 변환 불가능! Exception 발생! 💥
// 발생하는 에러
HttpMessageNotReadableException: Content type 'text/plain' not supported for bodyType=User
RequestMappingHandlerAdapter (컨트롤러 호출 담당)
├── ArgumentResolver들
│ ├── RequestBodyArgumentResolver (@RequestBody 처리)
│ ├── RequestParamArgumentResolver (@RequestParam 처리)
│ └── ModelAttributeArgumentResolver (@ModelAttribute 처리)
│
├── HttpMessageConverter들 (ArgumentResolver가 사용)
│ ├── ByteArrayHttpMessageConverter
│ ├── StringHttpMessageConverter
│ └── MappingJackson2HttpMessageConverter
│
└── ReturnValueHandler들
├── RequestResponseBodyMethodProcessor (@ResponseBody 처리)
└── ModelAndViewMethodReturnValueHandler (View 처리)
// 1. ArgumentResolver가 @RequestBody를 발견
// 2. "HttpMessageConverter야, 이거 변환해줘!"
// 3. HttpMessageConverter가 JSON → 객체 변환
// 4. Controller 메서드 실행
// 5. ReturnValueHandler가 결과 처리
// 6. "HttpMessageConverter야, 응답도 변환해줘!"
// 7. HttpMessageConverter가 객체 → JSON 변환
// ✅ 올바른 방법
fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json' // 필수!
},
body: JSON.stringify({name: '김개발', age: 25})
});
// ❌ 잘못된 방법 (Content-Type 누락)
fetch('/api/users', {
method: 'POST',
body: JSON.stringify({name: '김개발', age: 25}) // 변환 실패!
});
// ✅ 명확한 타입 지정
@PostMapping(
value = "/api/users",
consumes = "application/json", // 요청은 JSON만 받겠다
produces = "application/json" // 응답도 JSON으로 주겠다
)
public User createUser(@RequestBody User user) {
return user;
}
// ❌ 애매한 설정
@PostMapping("/api/users") // 뭘 받을지, 뭘 줄지 불분명
public Object createUser(@RequestBody Object data) {
return data;
}
# application.yml에 추가
logging:
level:
org.springframework.web.servlet.mvc.method.annotation: DEBUG
org.springframework.http.converter: DEBUG
// 커스텀 MessageConverter 예시
public class CsvHttpMessageConverter implements HttpMessageConverter<List<String[]>> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return List.class.isAssignableFrom(clazz) &&
MediaType.parseMediaType("text/csv").isCompatibleWith(mediaType);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return List.class.isAssignableFrom(clazz) &&
MediaType.parseMediaType("text/csv").isCompatibleWith(mediaType);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return Arrays.asList(MediaType.parseMediaType("text/csv"));
}
@Override
public List<String[]> read(Class<? extends List<String[]>> clazz,
HttpInputMessage inputMessage) throws IOException {
// CSV 파싱 로직
BufferedReader reader = new BufferedReader(new InputStreamReader(inputMessage.getBody()));
return reader.lines()
.map(line -> line.split(","))
.collect(Collectors.toList());
}
@Override
public void write(List<String[]> data, MediaType contentType,
HttpOutputMessage outputMessage) throws IOException {
// CSV 쓰기 로직
PrintWriter writer = new PrintWriter(outputMessage.getBody());
data.forEach(row -> writer.println(String.join(",", row)));
writer.flush();
}
}
// WebConfig에 등록
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CsvHttpMessageConverter());
}
}
@PostMapping("/api/data")
public ResponseEntity<?> handleData(@RequestBody Object data,
HttpServletRequest request) {
String contentType = request.getContentType();
if ("application/json".equals(contentType)) {
// JSON으로 처리된 데이터
return ResponseEntity.ok("JSON 처리 완료");
} else if ("application/xml".equals(contentType)) {
// XML로 처리된 데이터
return ResponseEntity.ok("XML 처리 완료");
}
return ResponseEntity.badRequest().body("지원하지 않는 형식");
}
@PostMapping(value = "/api/users",
consumes = {"application/json", "application/xml"})
public User createUser(@RequestBody User user) {
// JSON이든 XML이든 자동으로 User 객체로 변환됨
return user;
}
@PostMapping("/test")
public String test(@RequestBody String data) {
return data;
}
// 요청: Content-Type: application/json
// Body: {"message": "hello"}
정답: StringHttpMessageConverter가 동작합니다!
@PostMapping("/user")
public User createUser(@RequestBody User user) {
return user;
}
// 요청: Content-Type: text/plain
// Body: {"name": "김개발"}
정답: Content-Type이 text/plain인데 JSON 데이터를 보냈기 때문!
@PostMapping("/upload")
public String uploadFile(@RequestBody byte[] fileData) {
return "업로드 완료";
}
정답: 파일 업로드에는 MultipartFile을 사용해야 함!
@RequestParam MultipartFile file 사용 권장HttpMessageConverter는 Spring MVC에서 HTTP와 Java 객체 간의 변환을 담당하는 핵심 컴포넌트입니다.
핵심 기억사항:
1. Content-Type 헤더 설정이 가장 중요
2. 우선순위 이해하기 (byte[] → String → Object)
3. @RequestBody/@ResponseBody와 함께 자동 동작
4. 커스텀 Converter로 확장 가능
이 가이드가 Spring HTTP 통신의 이해에 도움이 되길 바랍니다! 🚀