본문 바로가기
Spring

jackson-dataformat-xml 이슈 정리

by ybs 2021. 10. 19.
반응형

jackson-dataformat-xml(2.12.4) 를 사용하면서 겪은 이슈를 정리했다. 

cf) 기존 코드는 jaxb-api(2.1) 을 사용중이었다.

1. interface 네이밍 

interface 네이밍은 자바에서 예약어기 때문에 xml element 로 좋은 네이밍은 아니다. 하지만 기존 코드와 똑같이 개발해야 했다. 먼저 바인딩 시킬 필드에 아래와 같이 @XmlElement 애노테이션을 붙이고 name을 interface로 지정했다.

@XmlElement(name = "interface")
private String interFace;

 

롬복을 썼기 때문에 자동으로 setter는 아래와 같이 setInterFace 네이밍으로 만들어졌다.

public void setInterFace(String interFace) {
	this.interFace = interFace;
}

 

하지만 xml interface 태그의 값이 제대로 바인딩 되지 않은 이슈가 발생했다. 대신 setter 네이밍을 아래와 같이 바꿔봤더니 정상적으로 바인딩 됐다. f 를 소문자로 바꾼것이다.

public void setInterface(String interFace) {
	this.interFace = interFace;
}

 

정확한 원인을 파악하기 위해 디버깅을 했다. 

_beanProperties 에서 'interface' 를 찾으려는데 결과가 안나와 prop 이 null 이 되고 handleUnknownVanilla 에서 com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "interface" 에러가 발생한다.

 

이 문제를 해결하기 위한 또 한가지 방법은 _beanProperties 에 있는 _caseInsensitive 속성을 이용하는 것이다.

objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); 설정을 켜서 대소문자를 구분하지 않게 하면 _beanProperties 에서 찾아진다.

 

그리고 @XmlElement 대신 @JacksonXmlProperty 을 사용하면 _beanProperties 에 interface로 등록되어 있어서 찾아진다.

@JacksonXmlProperty(localName = "interface")
private String interFace;

하지만  javax.xml.bind.annotation 의존성 말고 직접적인 com.fasterxml.jackson.dataformat.xml.annotation 의존성을 추가해주는것은 좋지 않다고 판단해서 사용하지 않았다.

 

2. list 이슈

같은 xml 태그 여러개가 input으로 오면 자바 List 객체로 바인딩 할 수 있다. 하지만 같은 xml 태그이지만 연속적으로 오지 않으면 다르게 동작한다. 아래 예제 코드를 보자.

package com.fasterxml.jackson.dataformat.xml.lists;

import java.util.List;

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

import com.fasterxml.jackson.annotation.JsonMerge;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlCData;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;

public class ListTest {
	public static void main(String[] args) throws JsonProcessingException {
		String xml = ""
			+ "<Data>"
			+ "    <product>a</product>"
			+ "    <product>b</product>"
			+ "    <backUrl>www.naver.com</backUrl>"
			+ "    <product>c</product>"
			+ "    <product>d</product>"
			+ "    <backUrl>www.naver.com</backUrl>"
			+ "    <product>e</product>"
			+ "</Data>";

		XmlMapper m = new XmlMapper();
		// m.setDefaultMergeable(true);
		Data data = m.readValue(xml, Data.class);
		System.out.println(data.getProduct());
	}
}

@JacksonXmlRootElement(localName = "data")
class Data {
	@JacksonXmlCData
	@JacksonXmlElementWrapper(useWrapping = false)
	@JacksonXmlProperty
	private List<String> product;

	@JacksonXmlCData
	@JacksonXmlElementWrapper(useWrapping = false)
	@JacksonXmlProperty
	private String backUrl;

	public List<String> getProduct() {
		return product;
	}

	public void setProduct(List<String> product) {
		this.product = product;
	}

	public String getBackUrl() {
		return backUrl;
	}

	public void setBackUrl(String backUrl) {
		this.backUrl = backUrl;
	}
}

 

여러개의 product 태그들이 List 객체로 바인딩 되어야 하는데 실제로 getProduct를 해보면 e 만 나온다. 일반적이지 않지만 외부 사용자가 보낸 요청을 받는 입장이라 문제없이 List 객체로 바인딩 되게 해야 했다. 제일 쉽게 할 수 있는건 setProduct 메서드를 고치는거다.

public void setProduct(List<String> product) {
	if (this.product == null) {
		this.product = product;
	} else {
		this.product.addAll(product);
	}
}

product 가 overwrite 되지 않도록 add 해주게 바꾸면 원하는 결과가 나온다. 

 

그리고 또 한가지 방법이 있다. jackson-dataformat-xml(2.9)부터 제공한 setDefaultMergeable 을 사용하면 된다.

setDefaultMergeable true 설정 X 결과 : [e]

setDefaultMergeable true 설정 O 결과 : [a, b, c, d, e]

하지만 setDefaultMergeable 를 사용할 땐 한가지 주의해야 할 점이 있다. Null 방어를 위해 product 에 대한 getter가 아래와 같이 되어 있다면 최초 this.product는 Null 이므로 getProduct 메서드 결과가 EMPTY_LIST 가 된다.

public List<String> getProduct() {
	return Objects.isNull(this.product) ? 
	Collections.emptyList() :
	Collections.unmodifiableList(this.product);
}

 

내부적으로 merge 를 할 때 getProduct를 호출 하는데, 결과가 EMPTY_LIST 로 받아져서(최초에) add 가 안되고org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: (was java.lang.UnsupportedOperationException); nested exception is com.fasterxml.jackson.databind.JsonMappingException: (was java.lang.UnsupportedOperationException) 에러가 발생한다(@JsonMerge 를 써도 에러 발생).

 

그러므로 getProduct를 그냥 기본형태로 구현하거나

public List<String> getProduct() {
	return this.product;
}

 

Null 리턴을 막기 위해 아래와 같이 작업해주면 된다.

public List<String> getProduct() {
	if (this.product == null) {
		this.product = new ArrayList<>();
	}
	return this.product;
}

 

cf) XmlMapper 는 ObjectMapper 하위 클래스다. XmlMapper.setDefaultMergeable(true) 로 설정해도 상위 ObjectMapper 가 구현한 setDefaultMergeable 메서드가 호출되기 때문에 상관없다.

 

참고 : https://github.com/FasterXML/jackson-dataformat-xml/issues/363 

반응형