[Spring MVC] spring boot와 spring legacy 에서 jackson-dataformat-xml 의 converting 우선순위가 다른 이유와 해결법

식빵·2022년 2월 17일
0

Spring Lab

목록 보기
8/33
post-thumbnail

설명의 Spring Legacy라는 표현은 Spring boot 환경이 아닌 프로젝트를 의미합니다.


Spring Legacy에서만 생기는 문제

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 bootSpring Legacy 환경에서 jackson-dataformat-xml
대한 차이를 불러 일으키는 걸까?




차이가 나는 핵심 원인

이유는 Spring Boot자동 설정 기능에 의해서 실행되는
HttpMessageConverters 클래스의 reorderXmlConvertersToEnd 메소드 때문이다.

이 메소드의 이름을 보면 알 수 있듯이 Spring Mvc 프레임워크가 내부적으로 사용하는
HttpMessageConverter 들 중에서도 jackson-dataformat-xml을 사용하는
HttpMessageConverter 의 적용 우선순위를 제일 아래로 낮춘다.

이에 반해서 Spring Legacy 프로젝트는 HttpMessageConverters 클래스 자체가 없다.
그리고 애초에 Spring Boot와 같은 자동 설정 기능도 당연히 없다.

그래서 Spring Legacyjackson-dataformat-xml을 사용하는 HttpMessageConverter 가 json을 반환하는 HttpMessageConverter 보다 우선순위가 높아서 xml이 반환되는 것이다.




원인 상세 파악

이유 알았지만 그래도 코드를 추적해가면서 보면 조금 더 와닿는다.
그러니 지금부터 코드를 추적해가면서 원인에 대한 상세 파악을 해보자.

  1. Spring Boot의 경우 jackson-dataformat-xml 추가 및 자동설정 코드 관찰
  2. Spring Legacy의 경우에는 어떨까?
  3. Spring LegacySpring Boot 처럼 동작하도록 설정하기



1. 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가 자체적으로 이렇게 우선순위를 잡는다.




2. Spring Legacy은 자동설정이 없다.

Spring boot자동 설정을 지원하기 때문에 위처럼 동작한다.
하지만 Spring Legacy 환경에서는 위와 같은 자동 설정이 없으므로,
같은 효과를 내기 위해서는 직접 설정해줘야 한다.




3. Spring Legacy도 Spring Boot처럼 동작시키기

방법은 간단하다.

위에서 본 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개를 참조하면 되겠다.

profile
백엔드를 계속 배우고 있는 개발자입니다 😊

2개의 댓글

comment-user-thumbnail
2023년 8월 2일

감사합니다!!

1개의 답글