무엇이 바뀌었길래 테스트가 깨지는가?
org.springframework.test.web.servlet.MockMvc 를 이용한 Mock 컨트롤러 테스트.
한글 인코딩이 깨진다.
MockMvc의 경우 perform이 수행되면 MockFilterChain -> HttpServlet -> TestDisplacherServlet -> FrameworkServlet -> HttpServlet - FrameworkServlet -> DispatcherServlet -> AbstractHandlerMethodAdapter ... 등을 거치다.
application/json의경우 HttpEntityMethodProcessor 의 handleReturnValue에서 AbstractMessageConverterMethodProcessor의 writeWithMessageConverters 메서드가 호출된다.
AbstractMessageConverterMethodArgumentResolver의 List<HttpMessageConverter<?>> 타입의 messageConverters 들이 있다.
스프링은 Jackson2가 클래스패스 상에 있으면 컨버터로 추가를 한다.
AbstractGenericHttpMessageConverter의 write() 메서드가 호출되면 AbstractGenericHttpMessageConverter 의 writeInternal() 메서드가 호출되는데, 하위의 AbstractJackson2HttpMessageConverter의 writeInternal()가 호출된다.
public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {
// ..
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding);
try {
writePrefix(generator, object);
this.objectMapper.getFactory().createGenerator 안쪽의 파라미터로 getBody()를 호출하는데 OutputStream 을 단순히 반환만 하지 않는다.
ublic class ServletServerHttpResponse implements ServerHttpResponse {
private boolean headersWritten = false;
private final HttpServletResponse servletResponse;
// ..
@Override
public OutputStream getBody() throws IOException {
this.bodyUsed = true;
writeHeaders();
return this.servletResponse.getOutputStream();
}
private void writeHeaders() {
if (!this.headersWritten) {
getHeaders().forEach((headerName, headerValues) -> {
for (String headerValue : headerValues) {
this.servletResponse.addHeader(headerName, headerValue);
}
});
// HttpServletResponse exposes some headers as properties: we should include those if not already present
if (this.servletResponse.getContentType() == null && this.headers.getContentType() != null) {
this.servletResponse.setContentType(this.headers.getContentType().toString());
}
if (this.servletResponse.getCharacterEncoding() == null && this.headers.getContentType() != null &&
this.headers.getContentType().getCharset() != null) {
this.servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name());
}
this.headersWritten = true;
}
}
writeHeaders()라는 것을 호출을 하는데 servletResponse 프로퍼티의 addHeader() 메서드를 호출해서 헤더를 추가하는 것을 알 수 있다.
HttpServletResponse 인터페이스를 MockMvc 의 경우 MockHttpServletResponse 로 구현을 하고 있는데 아래와 같다.
public class MockHttpServletResponse implements HttpServletResponse {
@Nullable
private String characterEncoding = WebUtils.DEFAULT_CHARACTER_ENCODING;
// ..
@Override
public void addHeader(String name, String value) {
addHeaderValue(name, value);
}
private void addHeaderValue(String name, Object value) {
if (setSpecialHeader(name, value)) {
return;
}
doAddHeaderValue(name, value, false);
}
private boolean setSpecialHeader(String name, Object value) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(value.toString());
return true;
}
// ..
SpecialHeader의 경우 setContentType이 호출이 되는데 characterEncoding 프로퍼티를 contentType의 캐릭터셋으로 덮어씌움을 알 수 있다.
public class MockHttpServletResponse implements HttpServletResponse {
// ..
@Override
public void setContentType(@Nullable String contentType) {
this.contentType = contentType;
if (contentType != null) {
try {
MediaType mediaType = MediaType.parseMediaType(contentType);
if (mediaType.getCharset() != null) {
this.characterEncoding = mediaType.getCharset().name();
this.charset = true;
}
}
// ..
스프링 5.1.6에서는 contentType이 application/json;charset=UTF-8 였다.
반면 5.2.0에서 org.springframework.http.MediaType#APPLICATION_JSON_UTF8 이 Deprecated되면서 application/json로 바뀌었다.
public class MockHttpServletResponse implements HttpServletResponse {
// ..
private void addHeaderValue(String name, Object value) {
boolean replaceHeader = false;
if (setSpecialHeader(name, value, replaceHeader)) {
return;
}
doAddHeaderValue(name, value, replaceHeader);
}
private boolean setSpecialHeader(String name, Object value, boolean replaceHeader) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(value.toString());
return true;
}
@Override
public void setContentType(@Nullable String contentType) {
this.contentType = contentType;
if (contentType != null) {
try {
MediaType mediaType = MediaType.parseMediaType(contentType);
if (mediaType.getCharset() != null) {
this.characterEncoding = mediaType.getCharset().name();
this.charset = true;
}
}
// ..
이전에는 contentType이 application/json 일경우 MediaType#APPLICATION_JSON 였다.
public class MimeType implements Comparable<MimeType>, Serializable {
private static final String PARAM_CHARSET = "charset";
@Nullable
public Charset getCharset() {
String charset = getParameter(PARAM_CHARSET);
return (charset != null ? Charset.forName(unquote(charset)) : null);
}
PARAM_CHARSET 이 없으니 mediaType.getCharset() 은 null이되어 characterEncoding 이 바뀌지 않는 것이다.
해결책
http://honeymon.io/tech/2019/10/23/spring-deprecated-media-type.html
'Programing > Framework' 카테고리의 다른 글
[Spring JPA] 멀티컬럼 - 쿼리메소드 vs @Query (0) | 2020.05.26 |
---|---|
[Spring] spring-retry 재시도 및 백오프 정책 정리 (0) | 2020.04.17 |
[spring integration] TCP 연결시간 설정 (0) | 2020.02.18 |
[Spring] mvc - DispatcherServlet 1부 (1) | 2020.02.09 |
[Spring] SSE vs WebFlux (0) | 2020.02.08 |