Spring HttpMessageConverter 완벽 가이드 - HTTP와 Java 객체 간 변환의 핵심

geoson·2025년 6월 5일

Spring & 백엔드

목록 보기
14/18

1. HttpMessageConverter란?

🎯 한 줄 정의

HTTP 요청/응답의 Body 데이터를 Java 객체로 변환하거나, 그 반대로 변환해주는 Spring의 핵심 컴포넌트

📝 쉬운 비유

우체부가 편지를 배달할 때, 한국어 편지를 영어로 번역하거나 영어 편지를 한국어로 번역해주는 번역가 역할

클라이언트 ↔ [HttpMessageConverter] ↔ Spring Controller
   JSON     ↔      [번역가]        ↔    Java 객체

2. 왜 필요한가?

🚨 문제 상황

// ❌ 이런 식으로는 할 수 없음
@PostMapping("/users")
public String createUser(String jsonString) {
    // JSON 문자열을 어떻게 User 객체로 바꿀까? 😵
    // "{\"name\":\"김개발\", \"age\":25}" → User 객체
}

✅ HttpMessageConverter 덕분에 가능한 것

// ✅ 자동으로 JSON → User 객체 변환!
@PostMapping("/users")
public User createUser(@RequestBody User user) {
    // 와! JSON이 자동으로 User 객체가 되었다! 🎉
    return user; // User 객체 → JSON 자동 변환!
}

🔄 실제 HTTP 통신 과정

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

3. 동작 원리

🌊 전체 플로우

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;
}

📖 동작 순서 (읽기)

  1. "이 데이터 변환할 수 있어?" - canRead() 호출
  2. "변환 가능하면 변환해줘!" - read() 호출

📝 동작 순서 (쓰기)

  1. "이 객체 응답으로 변환할 수 있어?" - canWrite() 호출
  2. "변환 가능하면 변환해줘!" - write() 호출

4. 주요 Converter 종류

📊 우선순위 순으로 정리

순위Converter처리 대상요청 타입응답 타입용도
1ByteArrayHttpMessageConverterbyte[]/application/octet-stream파일, 이미지
2StringHttpMessageConverterString/text/plain단순 텍스트
3ResourceHttpMessageConverterResource/-정적 리소스
4FormHttpMessageConverterMultiValueMapapplication/x-www-form-urlencoded-폼 데이터
5MappingJackson2HttpMessageConverterObjectapplication/jsonapplication/jsonJSON (가장 중요!)

💡 기억법

  • 바이트문자열객체 순서
  • 단순한 것부터 → 복잡한 것 순서
  • 우선순위가 높을수록 먼저 체크됨!

5. 실습 예제

✅ 예제 1: 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 객체로 변환 완료!

✅ 예제 2: JSON 문자열로 받기 (성공)

// 같은 요청이지만 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보다 먼저)

❌ 예제 3: 실패 케이스

// 요청 데이터
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

6. 내부 구조 깊이보기

🏗️ Spring MVC 전체 구조

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 변환

💡 핵심 포인트

  • HttpMessageConverter는 도구일 뿐!
  • 실제 호출은 ArgumentResolver와 ReturnValueHandler가 담당
  • "누가 언제 사용하는가?"가 더 중요

7. 실무 활용법

🛠️ 1. 올바른 Content-Type 설정

// ✅ 올바른 방법
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})  // 변환 실패!
});

🛠️ 2. Controller에서 명시적 타입 지정

// ✅ 명확한 타입 지정
@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;
}

🛠️ 3. 디버깅하기

# application.yml에 추가
logging:
  level:
    org.springframework.web.servlet.mvc.method.annotation: DEBUG
    org.springframework.http.converter: DEBUG

🛠️ 4. 커스텀 Converter 만들기

// 커스텀 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());
    }
}

8. 고급 활용 - 조건부 변환

📋 요청 헤더에 따른 다른 처리

@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;
}

💡 핵심 정리

✨ 꼭 기억할 것들

  1. HttpMessageConverter = HTTP Body 변환 담당자
  2. 우선순위: byte[] → String → Object
  3. @RequestBody, @ResponseBody에서 자동 동작
  4. Content-Type 헤더가 매우 중요!
  5. JSON 변환은 MappingJackson2HttpMessageConverter 담당

🚀 다음 단계

  • ArgumentResolver와 ReturnValueHandler 깊이보기
  • TypeConverter와 Formatter 이해하기
  • 커스텀 MessageConverter 만들어보기
  • Spring WebFlux의 CodecConfigurer 학습

🧩 연습 문제

Q1. 다음 코드에서 어떤 MessageConverter가 동작할까?

@PostMapping("/test")
public String test(@RequestBody String data) {
    return data;
}

// 요청: Content-Type: application/json
// Body: {"message": "hello"}

정답: StringHttpMessageConverter가 동작합니다!

  • JSON 형태이지만 받는 타입이 String이므로
  • 우선순위에 따라 String 변환기가 먼저 체크되어 선택됨

Q2. 이 요청이 실패하는 이유는?

@PostMapping("/user")
public User createUser(@RequestBody User user) {
    return user;
}

// 요청: Content-Type: text/plain
// Body: {"name": "김개발"}

정답: Content-Type이 text/plain인데 JSON 데이터를 보냈기 때문!

  • MappingJackson2HttpMessageConverter는 application/json만 처리 가능
  • Content-Type을 application/json으로 변경해야 함

Q3. 이 코드의 문제점은?

@PostMapping("/upload")
public String uploadFile(@RequestBody byte[] fileData) {
    return "업로드 완료";
}

정답: 파일 업로드에는 MultipartFile을 사용해야 함!

  • byte[] 방식은 메모리 사용량이 과도할 수 있음
  • @RequestParam MultipartFile file 사용 권장

🎯 결론

HttpMessageConverter는 Spring MVC에서 HTTP와 Java 객체 간의 변환을 담당하는 핵심 컴포넌트입니다.

핵심 기억사항:
1. Content-Type 헤더 설정이 가장 중요
2. 우선순위 이해하기 (byte[] → String → Object)
3. @RequestBody/@ResponseBody와 함께 자동 동작
4. 커스텀 Converter로 확장 가능

이 가이드가 Spring HTTP 통신의 이해에 도움이 되길 바랍니다! 🚀

0개의 댓글