spring:spring-web 3.2.2 에서 FormHttpMessageConverter에서 사용하는 partConverters 중 StringHttpMessageConverter 는 기본 charset이 ISO-8859-1 이다. 이로 인해서 Spring RestTemplate 을 통해서 multipart/form-data 형식으로 요청할 경우 파라미터의 인코딩이 깨지게 된다.
분석을 위해 코드를 열어보자.
RestTemplate 에서 별도 설정 없이 multipart/form-data 형식의 요청을 날리면, FormHttpMessageConverter 을 확장한 AllEncompassingFormHttpMessageConverter 를 통해서 컨버팅되어진다. 이때 AllEncompassingFormHttpMessageConverter 에서는 xml, json 등 타입에 따라 FormHttpMessageConverter.partConverters 외에도 더 필요한 컨버터가 있다면 add해주는 역할만 할뿐 별다른 행위를 하지는 않는다. 문제는 FormHttpMessageConverter 기본 생성자에 있다.
아래는 org.springframework.http.converter.FormHttpMessageConverter 의 생성자 부분 코드이다.
...
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
private static final byte[] BOUNDARY_CHARS =
new byte[]{'-', '_', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z'};
private final Random rnd = new Random();
private Charset charset = Charset.forName("UTF-8");
private List<MediaType> supportedMediaTypes = new ArrayList<MediaType>();
private List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
public FormHttpMessageConverter() {
this.supportedMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
this.supportedMediaTypes.add(MediaType.MULTIPART_FORM_DATA);
this.partConverters.add(new ByteArrayHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(); <------ StringHttpMessageConverter 를 기본 생성자를 이용하여 인스턴스화 함
stringHttpMessageConverter.setWriteAcceptCharset(false);
this.partConverters.add(stringHttpMessageConverter);
this.partConverters.add(new ResourceHttpMessageConverter());
}
/**
* Set the message body converters to use. These converters are used to convert objects to MIME parts.
*/
public final void setPartConverters(List<HttpMessageConverter<?>> partConverters) {
Assert.notEmpty(partConverters, "'partConverters' must not be empty");
this.partConverters = partConverters;
}
/**
* Add a message body converter. Such a converters is used to convert objects to MIME parts.
*/
public final void addPartConverter(HttpMessageConverter<?> partConverter) {
Assert.notNull(partConverter, "'partConverter' must not be NULL");
this.partConverters.add(partConverter);
}
...
화살표로 표시한것 처럼 StringHttpMessageConverter 를 기본 생성자를 이용하여 인스턴스화 하여 partConverters 에 add 하고 있다.
아래는 org.springframework.http.converter.StringHttpMessageConverter 의 생성자 부분 코드이다.
...
public class StringHttpMessageConverter extends AbstractHttpMessageConverter<String> {
public static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); <------ 헉 찾았다..
private final Charset defaultCharset;
private final List<Charset> availableCharsets;
private boolean writeAcceptCharset = true;
/**
* A default constructor that uses {@code "ISO-8859-1"} as the default charset. <------ 미국에서 태어났더라면...삽질을 안했을텐데.
* @see #StringHttpMessageConverter(Charset)
*/
public StringHttpMessageConverter() {
this(DEFAULT_CHARSET);
}
/**
* A constructor accepting a default charset to use if the requested content
* type does not specify one.
*/
public StringHttpMessageConverter(Charset defaultCharset) {
super(new MediaType("text", "plain", defaultCharset), MediaType.ALL);
this.defaultCharset = defaultCharset;
this.availableCharsets = new ArrayList<Charset>(Charset.availableCharsets().values());
}
...
자, 그럼 어떻게 이 난관을 극복했는지 살펴보자.
...
this.restTemplate = new RestTemplate();
/* FormHttpMessageConverter > partConverters > StringHttpMessageConverter 의 기본 인코딩타입이 ISO-8859-1 여서 multipart/form-data 로 파라미터를 넘길 경우 한글인코딩이 깨짐 */
for (HttpMessageConverter<?> hmc : restTemplate.getMessageConverters()) {
if (hmc instanceof AllEncompassingFormHttpMessageConverter) {
/** AllEncompassingFormHttpMessageConverter 생성자 내용 일부 가져와서 수정 **/
List<HttpMessageConverter<?>> partConverterList = new ArrayList<HttpMessageConverter<?>>();
partConverterList.add(new ByteArrayHttpMessageConverter());
StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("UTF-8")); <------ StringHttpMessageConverter Charset 오버로딩 생성자를 통해서 해결하였다.
stringHttpMessageConverter.setWriteAcceptCharset(false);
partConverterList.add(stringHttpMessageConverter);
partConverterList.add(new ResourceHttpMessageConverter());
partConverterList.add(new SourceHttpMessageConverter());
if (ClassUtils.isPresent("javax.xml.bind.Binder", AllEncompassingFormHttpMessageConverter.class.getClassLoader())) {
partConverterList.add(new Jaxb2RootElementHttpMessageConverter());
}
if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader())
&& ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader())) {
partConverterList.add(new MappingJackson2HttpMessageConverter());
}
else if (ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader())
&& ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", AllEncompassingFormHttpMessageConverter.class.getClassLoader())) {
partConverterList.add(new MappingJacksonHttpMessageConverter());
}
((AllEncompassingFormHttpMessageConverter) hmc).setPartConverters(partConverterList);
}
}
...
그닥 깊은 설명은 필요없을듯하다. 사실 http client 를 직접 구현하기 싫어서 spring restclient 를 사용한건데 인코딩 땜에 너무 진을 뺏다. 게다가... 버전이 3.2.2 라니... 이휴... 언젠가는 잊혀질 공룡코드로 남겠지만 잠시 머릿속에 각인을 위해 남겨본다.