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
'Spring' 카테고리의 다른 글
WebClient 사용할때 주의 (4편) (0) | 2021.11.05 |
---|---|
여러 API 결과 조합(with 비동기) (0) | 2021.10.31 |
@RequestParam 사용 시 Null에 대한 고민 정리 (0) | 2021.09.20 |
@RequestParam 사용 시 주의사항 (2) | 2021.09.11 |
WebClient 사용할때 주의 (3편) (0) | 2021.06.23 |