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 라니... 이휴... 언젠가는 잊혀질 공룡코드로 남겠지만 잠시 머릿속에 각인을 위해 남겨본다.
덕분에 한글 문제 해결되었어요
그런데 파일인코딩이 여전히 깨집니다. multipartfile.getorinalfilename은 인코딩이 적용이안되네여
???_image.jpg 이런식으로 파일이름이 한글이 깨져서 나옵니다