본문 바로가기

Programing/Framework

[spring boot] 2.1.6 -> 2.2.0 테스트 깨짐(인코딩)

무엇이 바뀌었길래 테스트가 깨지는가?

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

https://stackoverflow.com/questions/55640629/how-to-make-spring-boot-default-to-application-jsoncharset-utf-8-instead-of-app