[Spring] Retrofit2에서 XML로 통신하기
요즘은 Http API를 주고받으면 데이터 형태는 XML보다 JSON으로 많이 사용하고 있다.
그렇지만 여전히 XML을 사용하는 회사도 많다. 외부 업체의 API가 XML로 데이터를 내려주기 때문에 XML로 통신할 수 있도록 개발해야했고, 우리는 Http client tool로 retrofit을 사용하고 있었다.
내가 해결해야 할 건 retrofit을 이용해서 XML로 통신할 수 있도록 만들어야 했다.
구글링을 해서 먼저 나온 건 SimpleXmlConverter을 사용할 수 있도록 com.squareup.retrofit2:converter-simplexml 의존성을 추가해서 사용하라는 글이 많다.
사용하려고 보니, deprecated 됐다.
친절하게 어떤걸 쓰는걸로 바꿔라! 라고 알려주어서 JAXB(Jakarta XML Binding) converter라는 녀석을 찾아보았다.
JAXB 또한 XML을 직렬화, 역직렬화를 간단하게 시켜주는 자바로 만들어진 라이브러리이다.
적용방법
코드는 Kotlin으로 작성했음.
1. 의존성 추가
// 의존성 추가
com.squareup.retrofit2:converter-jaxb:{자신이 사용하는 retrofit 버전에 맞게}
build.gradle에 jaxb 의존성을 추가해준다.
2. Retrofit 인스턴스 생성 시 Jaxb converter 추가
val retrofit = Retrofit.Builder()
.baseUrl("기본 url 변수")
.callFactory(OkHttpClient.Builder().build())
.addConverterFactory(JaxbConverterFactory.create()) // ConverterFactory로 Jaxb converter 추가
.build()
retrofit 생성 시, JaxbConverterFactory.create()를 통해 converter를 추가한다.
3. 클래스, 필드 바인딩
아래와 같은 XML 결과값이 온다고 가정하고, 이를 어떻게 바인딩 하는지 살펴봅시다. (간략한 예시일뿐, 모델링이 아님)
<?xml version="1.0" encoding="utf-8"?>
<coffee>
<no>34531</no>
<name>
<![CDATA[에티오피아에서 온 에스프레소]]>
</name>
<price>
<![CDATA[4500]]>
</price>
<saleStartDate>
<![CDATA[2020-08-01 00:00:00]]>
</saleStartDate>
<saleEndDate>
<![CDATA[2020-12-31 23:59:59]]>
</saleEndDate>
<images>
<image>
<![CDATA[https://www.zorbacoffee.kr/coffees/images/aslkdfjasl1293wens.jpg]]>
</image>
<image>
<![CDATA[https://www.zorbacoffee.kr/coffees/images/23weandawens.jpg]]>
</image>
</images>
<categories>
<category>
<code>4242</code>
<name>
<![CDATA[커피]]>
</name>
<origin>
<![CDATA[에티오피아]]>
</origin>
</category>
</categories>
</coffee>
Date 관련해서는 따로 정리를 위해서 saleStartDate, saleEndDate 필드는 생략하고 먼저 보자.
Coffee 클래스
@XmlRootElement(name = "coffee")
@XmlAccessorType(XmlAccessType.FIELD)
data class Coffee(
val no: Long? = null,
val name: String? = null,
val price: BigDecimal = BigDecimal.ZERO
@field:XmlElementWrapper(name = "images")
@field:XmlElement(name = "image")
val images: List<String>? = arrayListOf(),
@field:XmlElementWrapper(name = "categories")
@field:XmlElement(name = "category")
val categories: List<Category>? = arrayListOf(),
)
Category 클래스
@XmlRootElement(name = "category")
@XmlAccessorType(XmlAccessType.FIELD)
data class Category(
val code: Long? = null,
val name: String? = null,
val origin: String? = null
)
jaxb로 역직렬화를 수행하기 위해서는 기본 생성자(no argument constructor)가 필요하다. 코틀린에서는 필드들마다 기본 값을 넣어주어야 기본 생성자가 생성되기 때문에 기본 값을 일일이 넣어줬다.
@XmlRootElement: 최상단 클래스, enum 타입에 사용한다. 위의 예시에서 coffee가 최상단 루트에 해당한다.
@XmlAccessorType: XML 형식으로 직렬화, 역직렬화할 때 어디까지 접근할지 설정한다. 4가지 접근 방식(PROPERTY, FIELD, PUBLIC_MEMBER, NONE)이 있는데, FIELD 접근 방식을 아마 많이 사용하지 않을까 싶다. FIELD 접근방식은 static, transient가 아닌 필드들에 자동으로 바인딩 해준다.
코틀린을 잘 다루지 못해서 처음에 헤맸는데, 코틀린에서 class, data class에서 property 또는 primary constructor parameter에 애너테이션을 붙일 경우, 여러 장소에 애너테이션이 붙을 수 있어서 원하는대로 동작하지 않을 수 있다.
이게 무슨 말이냐...하면 공식 사이트에서는 use-site target라는 용어로 설명하고 있다. 코틀린에서 property 또는 primary constructor parameter에 있는 녀석들이 다양한 java elements로 만들어지기 때문에 여러 곳에 애너테이션이 붙을 수 있다고 한다. getter나 setter 등을 자동으로 생성해주기 때문에 getter나 setter 이외에 요소들에도 애너테이션이 붙는다는 의미이다.
위의 예시처럼 images, image라는 이름을 가진 필드에게만 이 애너테이션이 붙기를 원한다!!라고 컴파일러에게 알려주는 것이다.
그래서 @field를 앞에 붙여야한다.
@XmlElementWrapper: XmlElement의 콜렉션을 처리하기 위해 사용하는데, XmlElement의 목록들을 wrapping 해준다.
@XmlElement: property 이름에 맞춰 java bean을 mapping 해준다. XmlElement 애너테이션이 붙지 않은 필드들은 XmlAccessorType을 FIELD로 설정했기 때문에 XML element와 이름이 일치하면 자동 mapping 해주기 때문에 붙이지 않았다.
4. Date 타입 다루기
LocalTime, LocalDate, LocalDateTime을 사용하기 위해서는 XmlAdapter가 필요하다.
XmlAdapter는 java type으로 커스텀하게 직렬화 역직렬화 해주는 어댑터이다.
XmlAdapter를 상속받아서 직렬화, 역직렬화를 어떻게 시킬지 marshal(직렬화), unmarshal(역직렬화) 메서드에 재정의 해주면 된다.
LocalDateTimeXmlAdapter 클래스
class LocalDateTimeXmlAdapter : XmlAdapter<String, LocalDateTime>() {
private val dateFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
override fun marshal(date: LocalDateTime): String = date.format(dateFormat)
override fun unmarshal(date: String): LocalDateTime = LocalDateTime.parse(date, dateFormat)
}
포맷 형식은 어떤 패턴으로 받는지에 따라 맞춰줘야한다. 내 경우는 yyyy-MM-dd HH:mm:ss 식으로 주고 받아야해서 위와 같은 패턴으로 작성했다.
직렬화 할 때는 String으로 바뀌고, 역직렬화할 때는 LocalDateTime으로 바뀌는 것이다.
LocalDate, LocalTime도 위와 같은 방식으로 만들어주면 된다. Coffee 클래스를 마저 완성하면 아래처럼 만들 수 있다.
Coffee 클래스
@XmlRootElement(name = "coffee")
@XmlAccessorType(XmlAccessType.FIELD)
data class Coffee(
val no: Long? = null,
val name: String? = null,
val price: BigDecimal = BigDecimal.ZERO
// @XmlJavaTypeAdapter 애너테이션을 붙이고 사용할 Adapter 클래스를 붙여준다.
@field:XmlJavaTypeAdapter(LocalDateTimeXmlAdapter::class)
val saleStartDate: LocalDateTime? = null,
@field:XmlJavaTypeAdapter(LocalTimeXmlAdapter::class)
val saleEndDate: LocalDateTime? = null,
@field:XmlElementWrapper(name = "images")
@field:XmlElement(name = "image")
val images: List<String>? = arrayListOf(),
@field:XmlElementWrapper(name = "categories")
@field:XmlElement(name = "category")
val categories: List<Category>? = arrayListOf(),
)
@XmlJavaTypeAdapter을 사용해 어떻게 직렬화, 역직렬화할지 XmlAdapter를 상속받아 구현한 LocalDateTimeXmlAdapter를 추가해주면 끝!
여기까지 Retrofit2에서 JAXB를 이용해 XML 형식으로 Http API 통신하는 법을 다뤘다.
출처:
https://kotlinlang.org/docs/reference/annotations.html#annotation-use-site-targets
https://github.com/eclipse-ee4j/jaxb-ri
https://github.com/square/retrofit/tree/master/retrofit-converters/jaxb