설명의
Spring Legacy
라는 표현은Spring boot
환경이 아닌 프로젝트를 의미합니다.
Spring Legacy
환경에서 Spring MVC
프레임워크를 사용해서 API 개발 시 json
으로만 데이터를 반환하다가, 필요에 의해서 xml
을 반환해야 하는 경우가 생긴다.
이런 경우에 검색하면 가장 먼저 나오는 건 분명 jackson-dataformat-xml
를 빌드툴(maven, gradle 등)의 의존성에 추가하는 방법일 것이다.
왜냐하면 저 의존성을 추가만 해도 xml
반환이 자동으로 되기 때문이다.
하지만 이러면 치명적인 문제가 발생한다 ☠️
Spring Legacy
환경에서 jackson-dataformat-xml
를 사용하면 기존에
@ResponseBody
또는 ResponseEntity<?>
를 사용해서 json
을 반환하던
API 들이 갑자기 모두 xml
을 반환하기 시작한다!
그런데 웃긴 건(?) Spring boot
프로젝트에서는 jackson-dataformat-xml
의존성을 추가하면 이런 현상이 일어나지 않는다!
대체 무엇이 Spring boot
와 Spring Legacy
환경에서 jackson-dataformat-xml
에
대한 차이를 불러 일으키는 걸까?
이유는 Spring Boot
의 자동 설정 기능에 의해서 실행되는
HttpMessageConverters
클래스의 reorderXmlConvertersToEnd
메소드 때문이다.
이 메소드의 이름을 보면 알 수 있듯이 Spring Mvc
프레임워크가 내부적으로 사용하는
HttpMessageConverter
들 중에서도 jackson-dataformat-xml
을 사용하는
HttpMessageConverter
의 적용 우선순위를 제일 아래로 낮춘다.
이에 반해서 Spring Legacy
프로젝트는 HttpMessageConverters
클래스 자체가 없다.
그리고 애초에 Spring Boot
와 같은 자동 설정 기능도 당연히 없다.
그래서 Spring Legacy
는 jackson-dataformat-xml
을 사용하는 HttpMessageConverter
가 json을 반환하는 HttpMessageConverter
보다 우선순위가 높아서 xml
이 반환되는 것이다.
이유 알았지만 그래도 코드를 추적해가면서 보면 조금 더 와닿는다.
그러니 지금부터 코드를 추적해가면서 원인에 대한 상세 파악을 해보자.
Spring Boot
의 경우 jackson-dataformat-xml
추가 및 자동설정 코드 관찰Spring Legacy
의 경우에는 어떨까?Spring Legacy
도 Spring Boot
처럼 동작하도록 설정하기spring boot 프로젝트를 켜고 아래처럼 의존성을 추가하자.
참고로 현재 사용 중인 spring boot 버전은 2.6.3
이다.
build.gradle 의존성
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml'
}
이후에 아래 처럼 클래스 하나를 생성해두고 spring boot 를 실행시켜보자.
테스트 설정 클래스
package ...
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
System.out.println("=======================================================");
converters.forEach(System.out::println);
System.out.println("=======================================================");
}
}
참고:
spring boot에서는 웹관련 설정을 조금 수정할 때는 @EnableWebMvc를 쓰면 안된다.
그냥implements WebMvcConfigurer
+@Configuration
만 작업해주면 된다!
콘솔 출력 결과
=======================================================
org.springframework.http.converter.ByteArrayHttpMessageConverter@5db07507
org.springframework.http.converter.StringHttpMessageConverter@61ff70ea
org.springframework.http.converter.StringHttpMessageConverter@1c83d465
org.springframework.http.converter.ResourceHttpMessageConverter@7125bfe5
org.springframework.http.converter.ResourceRegionHttpMessageConverter@5da70d2c
org.springframework.http.converter.xml.SourceHttpMessageConverter@36153d2
org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter@1c39f27
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@688414fb
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter@5dc5d26d
org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter@356898ab
org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter@386e6a94
=======================================================
Spring MVC
프레임워크는 위에 보이는 우선순위로 실제 HttpMessageConverter
를 사용한다.
위로 갈 수록 우선순위가 높고, 아래로 갈수록 낮다.
참고: 위의 코솔 출력결과를 보면 중복이 보이는데, 이건 극히 정상적인 것이니 걱정 X
HttpMessageConverters 생성자의 converters 파라미터 설명을 보면 해당 내용이 나온다.// HttpMessageConverters 클래스 내부 /** * Create a new {@link HttpMessageConverters} instance with the specified converters. * @param addDefaultConverters if default converters should be added * @param converters converters to be added. Items are added just before any default * converter of the same type (or at the front of the list if no default converter is * found). The {@link #postProcessConverters(List)} method can be used for further * converter manipulation. */ public HttpMessageConverters(boolean addDefaultConverters, Collection<HttpMessageConverter<?>> converters) { List<HttpMessageConverter<?>> combined = getCombinedConverters(converters, addDefaultConverters ? getDefaultConverters() : Collections.emptyList()); combined = postProcessConverters(combined); this.converters = Collections.unmodifiableList(combined); }
상세한 내용은 모르겠지만, spring boot가 자체적으로 결정한 것이니 신경쓰지 말자.
이런 우선순위가 잡히는 이유는 스프링 부트에서 제공하는 자동 설정에 의한 것이다.
아래 코드를 보면 이해가 될 것이다.
package org.springframework.boot.autoconfigure.http; // spring boot 패키지이다!
// import 모두 생략
public class HttpMessageConverters implements Iterable<HttpMessageConverter<?>> {
//... 생략 ...
// 기본으로 제공하는 HttpMessageConverter 정보를 쫘악 읽어오는 메소드
private List<HttpMessageConverter<?>> getDefaultConverters() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
if (ClassUtils.isPresent("org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport",
null)) {
converters.addAll(new WebMvcConfigurationSupport() {
public List<HttpMessageConverter<?>> defaultMessageConverters() {
return super.getMessageConverters();
}
}.defaultMessageConverters());
}
else {
converters.addAll(new RestTemplate().getMessageConverters());
}
reorderXmlConvertersToEnd(converters); // !!*** 이게 핵심 ***!!
return converters;
}
// 얘가 HttpMessageConverter 중에서도 XML을 맡는 애들을 converter 우선순위 제일 뒤로 보내버리는 애다.
private void reorderXmlConvertersToEnd(List<HttpMessageConverter<?>> converters) {
List<HttpMessageConverter<?>> xml = new ArrayList<>();
for (Iterator<HttpMessageConverter<?>> iterator = converters.iterator(); iterator.hasNext();) {
HttpMessageConverter<?> converter = iterator.next();
if ((converter instanceof AbstractXmlHttpMessageConverter)
|| (converter instanceof MappingJackson2XmlHttpMessageConverter)) {
xml.add(converter);
iterator.remove();
}
}
converters.addAll(xml);
}
//... 생략 ...
}
이건 spring boot
실행 시 기본으로 동작하는 것이다.
보면 알겠지만, HttpMessageConverter는 여러개 중에서 xml 로 변환하는 converter의
우선순위를 제일 아래로 보내는 것을 확인할 수 있다.
그래서 jackson-dataformat-xml
의존성을 gradle에 추가해도 기존에 쓰던대로,
Controller 메소드에서 @ResponseBody
(또는 ResponseEntity
)를 사용하면 json
이 반환된다.
xml의 경우 Controller 메소드에 @GetMapping(produce=application/xml)
와 같은 설정을 추가하여 xml
을 반환 받을 수 있다.
json
을 사용 빈도가 높아서 spring boot
가 자체적으로 이렇게 우선순위를 잡는다.
Spring boot
는 자동 설정을 지원하기 때문에 위처럼 동작한다.
하지만 Spring Legacy
환경에서는 위와 같은 자동 설정이 없으므로,
같은 효과를 내기 위해서는 직접 설정해줘야 한다.
방법은 간단하다.
위에서 본 reorderXmlConvertersToEnd
메소드를 그대로 복사해서 가져오고,
spring mvc
프레임워크 초기화 과정에서 이 메소드를 사용하면 된다.
그러기 위해서 아래처럼 @Configuration
클래스를 작성하고
해당 클래스가 component scanning
이 되도록하면 끝이다.
@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// XML 관련 HttpMessageConverter 의 우선순위를 최하위로 낮추는 메소드
reorderXmlConvertersToEnd(converters);
// MappingJackson2XmlHttpMessageConverter 가 맨밑에 위치하는지 확인
converters.forEach(System.out::println);
}
private void reorderXmlConvertersToEnd(List<HttpMessageConverter<?>> converters) {
List<HttpMessageConverter<?>> xml = new ArrayList<>();
for (Iterator<HttpMessageConverter<?>> iterator =
converters.iterator(); iterator.hasNext();) {
HttpMessageConverter<?> converter = iterator.next();
if ((converter instanceof AbstractXmlHttpMessageConverter)
|| (converter instanceof MappingJackson2XmlHttpMessageConverter)) {
xml.add(converter);
iterator.remove();
}
}
converters.addAll(xml);
}
}
참고로 확장자를 사용하는 xml, json 반환 방식은 Spring 5.2.4
부터 Deprecated
됐으니 사용하지 말자. 관련글 링크는 아래 2개를 참조하면 되겠다.
감사합니다!!