Spring

[ Spring ] HttpMessageConverter 파헤치기 - 개발 지식

yongyongcoding 2025. 4. 18. 09:20

사용자로부터 서버는 값을 어떻게 받을 수 있을까?
서버는 사용자한테 값을 어떻게 넘길 수 있을까?

만약 서버는 LocalDate 클래스로 시간 정보를 받고 싶은데 Client는 LocalDate클래스가 뭔지  모르는데 어떻게 값을 넘겨야 할까? 또는 만약 사용자가 서버에게 User라는 값을 넘기고 싶을 때는 어떻게 해야할까?

 

Spring에서는 이러한 문제를 HttpMessageConverter를 통해 값을 변환해서 각자가 받기 좋은 데이터를 변환을 해줍니다.

사용자가 Json형태로 User정보를 넘기면 Spring이 Converter를 통해 서버에게 User객체로 만들어서 전달해줍니다.

오늘은 이러한 Converter에 대해서 알아보도록 하겠습니다! 

목차

01. HttpMessageConverter란? (서버와 사용자 간의 통역사!)
	01-1. 다양한 HttpMessageConverter
	01-2. 직렬화와 역직렬화 


02. MappingJackson2httpMessageConvert 사용 예시 
	02-1. @JsonProperty (필드명 바꾸기!)
    	02-2.@JsonCreator (역직렬화, 객체 만드는 방법 지정!)
    	02-3. @JsonSerialize (커스텀 직렬화!)
    	02-4. @JsonDeserialize (커스텀 역직렬화!)

03. 새로운 Converter만들어서 적용하기!

04. 동작 흐름 정리 (전체 요청-응답 과정에서의 역할)

05.Content Negotiation( 사용자가 포맷을 정하는 컨텐트 협상! )
	05-1. 요청 URL에 확장자를 사용 (현재 X)
	05-2. 요청에서 URL 매개 변수 사용 (예 : ? format = json )
	05-3. 요청에서 Accept 헤더 사용 (restful 원칙에 가장 적합)

 

 

 


 

01. HttpMessageConverter란? (서버와 사용자 간의 통역사!)

정의 : HTTP 요청과 응답에서 객체 <-> 문자열/Json/바이너리 등의 형태로 변환해주는 인터페이스입니다.

※ RestController에서 "hello" 문자열을 응답할 수 있었던 이유!

 

 

01-1. 다양한 HttpMessageConverter

Spring에는 기본적으로 다양한 HttpMessageConverter가 등록되어 있어서 자동으로 적용이 되고 있습니다.

WebConfig파일에서 configureMessageConverters 메소드를 보면 인자값으로  converters 리스트가 존재하는데 이 리스트 내부에 8개 정도의 기본적인 converter들이 들어 있습니다.

클래스 역할
MappingJackson2HttpMessageConverter JSON 변환 (Jackson 사용)
StringHttpMessageConverter 단순 문자열 변환
ByteArrayHttpMessageConverter byte 배열 변환
FormHttpMessageConverter application/x-www-form-urlencoded 처리
Jaxb2RootElementHttpMessageConverter XML 변환 (JAXB 기반)

 

★ Converters 리스트에 새로운 converter를 만들어서 추가, 삭제도 가능합니다.

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        System.out.println(converters);
        converters.add(new CsvMemberHttpMessageConverter());
        converters.add(new CsvMemberListHttpMessageConverter());
    }

 

 

 

 

01-2. ✨ 직렬화와 역직렬화 ✨

이렇게 conveter를 통해 Object와 문자열 데이터 사이의 변화를 직렬화/역직렬화라고 합니다.

http에서 값을 주고 받는데 아주 중요한 개념입니다!

 

  • 직렬화 : 객체 -> 문자열 
    • 객체를 저장하거나 전송하기 위해 바이트 또는 문자열 형태로 변환하는 개념입니다.
    • 서버는 User정보를 객체로 저장하고 있어 보내려면 객체로 보내야하지만 클라이언트는 User객체를 받지 못하기 때문에 문자열이나 Json, xml, csv 형태로 변환해서 전송해야합니다.
  • 역직렬화 : 문자열 -> 객체 
    • 직렬화된 데이터를 다시 객체로 복원하는 개념입니다.
    • 클라이언트가 Json형태로 User데이터를 보내면 데이터를 서버에서는 User객체로 저장해야하기 때문에 User객체로 변환하여 받습니다 

 

 

 


02. MappingJackson2httpMessageConvert 사용 예시 

아래의 어노테이션들은 jackson 라이브러리에서 직렬화/역직렬화 시에 아주 유용하게 사용하는 어노테이션들입니다.

 

02-1. @JsonProperty (필드명 바꾸기!)

역할 : 자바의 필드명과 Json에서 표현하고 싶은 필드명이 다를 경우 변환해주는 개념입니다.

 

아래와 같이 Json에서 표현할 때는 user_name으로 / java에서는 userName으로 다르게 표현할 수 있습니다.

public class User {
    @JsonProperty("user_name") // JSON: "user_name" ↔ Java: userName
    private String userName;

    @JsonProperty("age")
    private int age;

    // Getter/Setter 생략
}

 

 

02-2.@JsonCreator (역직렬화, 객체 만드는 방법 지정!)

역할 : Json을 객체로 생성시 생성자를 지정하는 방법입니다.

 

Json → Java 객체로 역직렬화할 때, 어떤 생성자나 팩토리 메서드를 사용할지 Jackson에게 알려주는 어노테이션이야.

언제 사용하나? : 보통은 기본 생성자 + setter 방식으로 역직렬화가 되지만, 불변 객체 (immutable) 이거나 생성자 기반으로 객체를 만들고 싶을 때 사용해.

{
  "name": "Alice",
  "age": 30
}
public class Person {
    private final String name;
    private final int age;

    @JsonCreator
    public Person(@JsonProperty("name") String name,
                  @JsonProperty("age") int age) {
        this.name = name;
        this.age = age;
    }

    // setter 없음 → 불변 객체
}

※  jackson은 기본 생성자가 없으면 어떤 생성자를 사용할지 모르기 때문에 기본적으로 기본생성자와 Setter를 필요로 하며 기본 생성자 대신에 @JsonCreator를 통해 생성자를 지정해 줄 수 있습니다.

※ 기본 생성자와 setter가 필요한 이유 : JSON → Java 객체로 만들 때 Jackson은 빈 껍데기 객체를 먼저 만든 다음
JSON의 key-value들을 하나씩 setter나 필드에 넣는 방식으로 동작해.

 

public enum ProjectType {
    PUBLIC, PRIVATE;

    @JsonCreator
    public static ProjectType fromString(String str){
        for(ProjectType value : ProjectType.values()){
            if(value.name().equals(str)){
                return value;
            }
        }
        return PUBLIC;
    }

    @JsonValue
    public String toJson(){
        return this.name().toLowerCase();
    }

}

※ 위와 같이 enum값에서 커스텀 생성자를 붙일 수 있습니다.

 

 

 

 

02-3. @JsonSerialize (커스텀 직렬화!)

역할 : 직렬화 할 때 사용하는 개념으로 커스텀한 방식으로 직렬화 할 수 있습니다.

 

언제 사용하나? : 자동 직렬화가 안되는 개념의 경우 커스텀한 방식으로 직렬화하여 사용할 수 있습니다.

02-4. @JsonDeserialize (커스텀 역직렬화!)

역할 : 역질렬화 할 때 사용하는 개념으로 @JsonSerialize와 동일하게 커스텀한 방식으로 역직렬화 할 수 있습니다.

 

언제 사용하나? :  @JsonSerialize와 동일하게 자동 역직렬화가 안되거나 커스텀한 방식으로 역직렬화 하고 싶을 때 사용합니다.

{
  "id": "user123",
  "name": "John",
  "age": 30,
  "class": "A",
  "locale": "ko"  // 소문자도 가능하게 처리됨
}

※ 위와 같이 Json을 받아야하는데 Locale 객체로 역직렬화가 불가능!

@JsonSerialize(using = ToStringSerializer.class)
@JsonDeserialize(using = LocaleDeserializer.class)
private Locale locale;

※ 위와 같이 선언하여 LocaleDeserializer.class를 만들어서 커스텀 역직렬화 진행!

public class LocaleDeserializer extends JsonDeserializer<Locale> {
    @Override
    public Locale deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        String value = p.getText().toUpperCase(); // 소문자로 들어와도 처리 가능하게
        return Locale.valueOf(value); // 예: "ko" → Locale.KO
    }
}

※ 값을 역직렬화 하여 클래스 값으로 변환해주면 됨.

 


 

지금까지 컨버터가 어떤 역할을 하는지, 컨버터 내부에서 어떤 방식으로 동작하는지도 알아보았습니다.

이번에는 새로운 Converter를 만들어서 적용해보겠습니다. 

 

03. 새로운 Converter만들어서 적용하기!

1) 새로운 컨버터 클래스 만들기

2) WebConfig에 새로운 컨버터 추가하기

[csv 포맷] 으로 직렬화/역질렬화 가능한 새로운 Converter를 만들어보겠습니다.

 

1. AbstractHttpMessageConverter 클래스의 상속.

AbstractHttpMessageConverter 클래스를 상속받는데 해당 클래스는 Spring에서 제공하는 HTTP 메세지 컨버터 추상 클래스입니다.

public class CsvMemberHttpMessageConverter extends AbstractHttpMessageConverter<MemberRequest> {

 

 

2. 생성자

    public CsvMemberHttpMessageConverter() {
        // 우리가 처리할 미디어타입: text/csv
        super(new MediaType("text", "csv", StandardCharsets.UTF_8));
    }

 

  • 이 컨버터는 Content-Type: text/csv 요청/응답에만 반응하도록 설정하는 부분입니다.
  • Spring이 자동으로 미디어타입을 기준으로 어떤 컨버터를 사용할지 결정하기 때문에 핵심 설정입니다

 

3. supports(Class<?> clazz) 메소드

@Override
protected boolean supports(Class<?> clazz) {
    return MemberRequest.class.isAssignableFrom(clazz);
}
  • 해당 Converter가 처리할 클래스인지 판단합니다. (해당 컨버터는 MemberRequest 클래스에 대해서만 처리합니다)

 

 

 

4. readInternal 메소드 

    @Override
    protected MemberRequest readInternal(Class<? extends MemberRequest> clazz,
                                         HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {

        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputMessage.getBody(), StandardCharsets.UTF_8))) {
            String line = reader.readLine();

            if (Objects.isNull(line) || line.isEmpty()) {
                throw new IllegalArgumentException("CSV 데이터가 비어있습니다.");
            }

            String[] tokens = line.split(",");
            if (tokens.length < 4) {
                throw new IllegalArgumentException("CSV 필드 개수가 부족합니다.");
            }

            return new MemberRequest(
                    tokens[0].trim(),               // id
                    tokens[1].trim(),               // name
                    Integer.parseInt(tokens[2].trim()), // age
                    tokens[3].trim()                // locale
            );
        }
    }
  • 처리하려는 csv 포맷으로 들어온 데이터를 MemberRequest객체로 변화해주는 역질렬화 메소드입니다.
  • 다음과 같이 inputMessage를 통해 csv 포맷 데이터를 읽고 객체로 만들어 반환해줍니다.

 

5. writeInternal 메소드

    @Override
    protected void writeInternal(MemberRequest memberRequest,
                                 HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {

        String csv = String.format("%s,%s,%d,%s",
                memberRequest.getId(),
                memberRequest.getName(),
                memberRequest.getAge(),
                memberRequest.getLocale());

        OutputStream outputStream = outputMessage.getBody();
        outputStream.write(csv.getBytes(StandardCharsets.UTF_8));
    }
  • 객체를 반환하고 싶은 특정 포맷을 직렬화하여 출력하는 메소드입니다.
  • 위의 경우 MemberRequest객체의 정보를 Csv 포맷으로 변환하여 반환해줍니다.

 

6. WebConfig에 이렇게 만든 클래스를 등록해주면 해당 객체에 대해 Context-Type이 등록한

MediaType인 text/csv일 경우 자동으로 해당 Converter가 적용이 됩니다.

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        System.out.println(converters);
        converters.add(new CsvMemberHttpMessageConverter());
        converters.add(new CsvMemberListHttpMessageConverter());
    }

 

 

 


04. 동작 흐름 (전체 요청-응답 과정에서의 역할)

  1. 클라이언트가 JSON 데이터 전송하면 
  2. DispatcherServlet이 컨트롤러에게 전달
  3. @RequestBody가 붙은 메서드 파라미터 감지 (Argument Resolver)
  4. 적절한 HttpMessageConverter가 동작해서 역질렬화(JSON → 객체 변환)하여 객체로 변환합니다.
  5. 비즈니스 로직 처리 후 객체 반환
  6. @ResponseBody 또는 RestController 감지
  7. 다시 HttpMessageConverter가 직렬화(객체 → JSON 변환)하여 JSON으로 변환합니다.
  8. 결과 value를 HTTP 응답으로 전송

 

 


 

지금까지 Conveter의 적용과 사용에 대해서 알아봤습니다.

지금까지 Server입장에서 Converter가 어떻게 작동하는지에 대해서 알아보았다면

이제는 Client입장에서 어떻게 원하는 자료형으로 값을 보내고 받을 수 있을까요.

사실 Spring에서 알아서 작동하지는 않습니다. 그렇기 때문에 Client가 직접 응답 Format을 정해줘야하는데
마지막으로 Client의 응답 포맷 설정에 대해서 알아보도록 하고 마무리 하도록 하겠습니다.

 

05.Content Negotiation( 사용자가 포맷을 정하는 컨텐트 협상! )

정의 : 클라이언트가 선호하는 표현 방식을 결정하는 역할입니다.
 
표현 방식에는 크게 3가지가 있습니다.
 
 
 

05-1. 요청 URL에 확장자를 사용 (현재 X)

  • /.xml이나 /.json 이런식으로 확장자를 적는 방법인데 Spring에서 현재는 사용중지한 방식입니다. (Spring 5.2~)

05-2. 요청에서 URL 매개 변수 사용 (예 : ? format = json )

  • 요청 파라미터 값으로 format으로 설정하는 방식입니다.
  • 파라미터 값을 넘겨줄 경우 아래와 같이 WebConfig파일에 설정을 해주어야합니다. 
   @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.parameterName("format")
                .favorParameter(true).defaultContentType(MediaType.APPLICATION_JSON)
                .mediaType("json", MediaType.APPLICATION_JSON)
                .mediaType("xml", MediaType.APPLICATION_XML);
    }

※ 파라미터 값에 따라 MediaType값을 설정해주는 것입니다.

 

05-3. 요청에서 Accept 헤더 사용 (restful 원칙에 가장 적합)

  • 아래와 같이 사용자가 Request 해더에 Accept값을 넣어서 함께 요청을 보내는 것입니다. 
### 멤버생성
POST localhost:8080/members
Accept: application/json
  • 반대로 보낼 데이터를 다르게 하고 싶을 때는 Content-Type 옵션을 넣어주면 됩니다.
### 멤버조회
GET localhost:8080/me
Content-Type : text/csv

 

 

 


오늘은 converter에 대해 깊이있게 알아보았습니다.

 

어떻게 문자열을 받으면 객체로 입력받을 수 있는지, 어떻게 객체를 반환했는데 사용자가 문자열로 받는지에 대해서 이해했고 또한 새로운 Converter를 만들어서 주고 받을 수 있는 새로운 형태를 만드는 방법도 알아보았습니다.

감사합니다.