티스토리 뷰
회사 프로젝트에서 SOAP 통신을 위해 XML를 다룰 일이 생겨서 정리한 포스팅입니다.
프로젝트 환경은 다음과 같습니다.
- Java 11
- Spring Boot 2.3.12
- Gradle 7.4.1
SOAP가 무엇일까?
- Simple Object Access Protocol의 약자
- XML 기반의 메시지를 컴퓨터 네트워크 상에서 교환하는 프로토콜
- 다양한 메시지 패턴이 있지만, 보통의 경우 RPC 패턴으로 통신
Spring MVC를 주로 사용하는 개발자로서는 REST가 더 친숙할텐데, REST에서는 주로 JSON 형태로 데이터를 주고받습니다.
🐻
REST와 SOAP의 차이점은 다른 포스팅에서 자세히 다루겠습니다.
최근 Open API는 대부분 REST API에 JSON 형태를 제공하지만, 비교적 옛날 Open API의 경우는 SOAP 통신만 지원하는 경우가 있습니다.
이러한 이유로, 저 또한 SOAP 통신을 위해 XML를 다루게 되었습니다.
코드를 먼저 보여드리고 순차적으로 설명하겠습니다.
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import java.io.StringWriter;
public class XmlUtil {
// 기본 생성자가 만들어지는 것을 막는다(인스턴스 방지용).
private XmlUtil() {
throw new InstantiationException();
}
/**
* obj를 XML 문자열로 변환하는 메서드
*
* @param obj 변환하려는 VO
* @param jaxbMarshaller marshal 객체
* @return 문자열(String)로 변환된 XML
*/
public static String convertObjectToXmlString(Object obj, Marshaller jaxbMarshaller) {
try {
StringWriter sw = new StringWriter();
jaxbMarshaller.marshal(obj, sw);
return sw.toString();
} catch (JAXBException e) {
throw new JAXBException();
}
}
}
private 생성자
effective java에서는 정적 메서드와 정적 필드만을 담은 유틸리티 클래스는 인스턴스로 만들어 쓰지 말라고 설명합니다.
컴파일러는 생성자를 명시하지 않으면 자동으로 public 기본 생성자를 만들어줍니다.
그렇기 때문에 해당 클래스는 정적 메서드만 제공할 예정이므로 private 생성자를 만들어주었습니다.
🐻
해당 XmlUtil 같은 경우는 정적 필드를 가지지 않기 때문에 인터페이스로 선언해도 괜찮아 보입니다.
(자바 8부터 지원)
Marshaller
마셜링이란 한 객체의 메모리에서 표현방식을 저장 또는 전송에 적합한 다른 데이터 형식으로 변환하는 과정을 의미합니다.
마셜링은 프로세스간 혹은 스레드간 데이터 전송에 필요한 RPC 메커니즘의 구현에도 사용된다고 합니다.
Marshaller 인터페이스에 marshal 메서드는 두 개의 파라미터를 가집니다.
- 변환하려는 VO 객체인 jaxbElement
- marshal 메서드의 결과를 담을 여러 종류의 그릇들
- java.io.Writer
- org.xml.sax.ContentHandler
- File
- java.io.OutputStream
- javax.xml.transform.Result
해당 프로젝트에서는 1번째인 Writer를 사용하였기 때문에 1번만 알아보겠습니다.
🐻
다른 그릇을 사용하실 분들은 spring docs를 참고해주세요!
XML namespace
코드를 더 살펴보기 전에 namespace를 먼저 다루겠습니다.
XML 네임스페이스는 XML 요소 간의 이름에 대한 충돌을 방지해 주는 방법을 제공합니다.
요소의 이름과 속성의 이름을 하나의 그룹으로 묶어주어 이름에 대한 충돌을 해결합니다.
XML에서는 접두사(prefix)를 이용하여 충돌을 방지합니다.
서로 같은 이름에 요소마다 서로 다른 접두사를 붙이면 이름의 충돌을 방지할 수 있게 됩니다.
XML에서 이러한 접두사를 사용하려면, 반드시 먼저 접두사에 대한 네임스페이스를 선언해야 합니다.
<요소이름 xmlns:prefix="URI">
XML 네임스페이스의 선언은 xmlns나 xmlns:로 시작합니다.
prefix 속성값에는 이름 앞에 붙게 되는 네임스페이스 접두사(namespace prefix)를 명시합니다.
접두사로 사용되는 URI는 네임스페이스 식별자를 의미합니다.
이제 VO를 XML로 만들어봅시다.
예제 XML은 다음과 같습니다.
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<kkoon9:kkoon9._.e xmlns:kkoon9="https://kkoon9.tistory.com/">
<STOCKLIST>
<STOCK_ID>123</STOCK_ID>
</STOCKLIST>
<STOCKLIST>
<STOCK_ID>456</STOCK_ID>
</STOCKLIST>
해당 XML로 만드는 테스트 코드를 살펴봅시다.
import com.sun.xml.bind.marshaller.NamespacePrefixMapper;
import com.sun.xml.bind.v2.runtime.IllegalAnnotationsException;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.*;
import static util.XmlUtil.convertObjectToXmlString;
import static util.XmlUtil.convertXmlStringToObject;
import static org.assertj.core.api.Assertions.assertThat;
class XmlUtilTest {
private static final String XML_ROOT_NAME = "kkoon9._.e";
private static final String XML_NAMESPACE = "<https://kkoon9.tistory.com/>";
private static final String PREFIX = "kkoon9";
private StockVO stock;
private JAXBContext jaxbContext;
private List stockItemIds = List.of("123", "456");
@BeforeEach
void init() throws JAXBException {
stock = new StockVO(items);
jaxbContext = JAXBContext.newInstance(stock.getClass());
}
@DisplayName("SOAP 통신을 위해 값 객체를 XML 문자열로 변환한다.")
@Test
void convert_object_to_xml_string() throws JAXBException {
// given
Marshaller jaxbMarshaller = jaxbContext.createMarshaller();
jaxbMarshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new NamespaceMapper());
jaxbMarshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
// when
String xmlString = convertObjectToXmlString(stock, jaxbMarshaller);
String expected = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" +
"<kkoon9:kkoon9._.e xmlns:kkoon9=\"https://kkoon9.tistory.com/\">\n" +
"<STOCKLIST>\n" +
" <STOCK_ID>123</STOCK_ID>\n" +
"</STOCKLIST>\n" +
"<STOCKLIST>\n" +
" <STOCK_ID>456</STOCK_ID>\n" +
"</STOCKLIST>" +
"</kkoon9:kkoon9._.e>";
// then
assertThat(xmlString).contains(expected);
}
@DisplayName("VO가 non-static일 때 에러를 던진다.")
@Test()
void convert_object_to_xml_string_non_static_error() {
// given
NonStaticStockVO stock = new NonStaticStockVO(items);
// then
Assertions.assertThatThrownBy(() -> JAXBContext.newInstance(stock.getClass()))
.isInstanceOf(IllegalAnnotationsException.class);
}
/**
* XML 변환 테스트를 위한 Sample VO
*/
@XmlRootElement(name = XML_ROOT_NAME, namespace = XML_NAMESPACE)
private static class StockVO {
@XmlElement(name = "STOCKLIST")
private List items = new ArrayList<>();
// XML 변경을 위해 NoArgsConstructor 필요
public StockVO() {
super();
}
public StockVO(List stockItemIds) {
super();
for (String stockItem : stockItemIds) {
items.add(new StockItemVo(stockItem));
}
}
static class StockItemVo {
@XmlElement(name = "STOCK_ID")
private String stockItemId;
public StockItemVo(String stockItemId) {
this.stockItemId = stockItemId;
}
public String toString() {
return stockItemId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StockItemVo that = (StockItemVo) o;
return Objects.equals(stockItemId, that.stockItemId);
}
@Override
public int hashCode() {
return Objects.hash(stockItemId);
}
}
}
/**
* non-static 시 에러를 보여주기 위한 non-static Sample VO
*/
@XmlRootElement(name = XML_ROOT_NAME, namespace = XML_NAMESPACE)
private class NonStaticStockVO {
@XmlElement(name = "STOCKLIST")
private List items = new ArrayList<>();
public NonStaticStockVO() {
super();
}
public NonStaticStockVO(List stockItemIds) {
super();
for (String stockItem : stockItemIds) {
items.add(new NonStaticStockItemVo(stockItem));
}
}
class NonStaticStockItemVo {
@XmlElement(name = "STOCK_ID")
private String stockItemId;
public NonStaticStockItemVo(String stockItemId) {
this.stockItemId = stockItemId;
}
}
}
// namespace prefix 명칭을 kkoon9로 바꾸기 위한 정적 클래스
private static class NamespaceMapper extends NamespacePrefixMapper {
private Map<String, String> prefixMap = new HashMap<>();
public NamespaceMapper() {
prefixMap.put(XML_NAMESPACE, PREFIX);
}
@Override
public String getPreferredPrefix(String namespaceUri, String suggestion,
boolean requirePrefix) {
return prefixMap.getOrDefault(namespaceUri, suggestion);
}
}
}
</string,>
init()
Marshaller를 사용하기 위해서 JAXBContext.getClass()를 사용해야 합니다.
getClass()에 파라미터는 VO의 runtime class를 넣어줍니다.
NamespaceMapper
jaxbContext.createMarshaller() 메서드를 이용하여 Marshaller 인스턴스를 만들어냅니다.
Marshaller 인스턴스의 setProperty() 메서드를 이용하여 namespace를 mapping해줍니다.
NamespaceMapper 클래스를 봐주세요!
Marshalling
위에서 설명한 convertObjectToXmlString 정적 메서드를 사용하여 VO를 xml로 변환해줍니다.
🐻
non-static VO로 JAXBContext를 만들려고 하면 에러가 발생합니다.
위 두 개의 테스트가 통과했습니다.
이번 포스팅에서는 XML과 SOAP를 가볍게 알아보고 marshalling하는 과정을 알아봤습니다.
다음 포스팅에서는 unmarshalling하는 과정을 알아보겠습니다.
'Sping Framework' 카테고리의 다른 글
Spring MVC lifecycle (0) | 2022.05.15 |
---|---|
XML In Java [2]. XML String to VO (0) | 2022.04.23 |
Spring Cloud Sleuth [2] - 분산 시스템과 동작 과정 (0) | 2022.04.06 |
Spring Cloud Sleuth [1] - 용어 (0) | 2022.04.04 |
Spring Boot에서 에러 처리하기 (0) | 2022.04.03 |
- Total
- Today
- Yesterday
- MSA
- 객체지향
- C++
- Kotlin
- 클린 아키텍처
- BAEKJOON
- JPA
- 클린 코드
- Spring Boot
- 디자인패턴
- programmers
- kkoon9
- 디자인 패턴
- Olympiad
- 코테
- Java
- Spring
- Algorithm
- 이펙티브 자바
- kotest
- BOJ
- Effective Java
- 테라폼
- AWS
- 알고리즘
- 정규표현식
- node.js
- 이팩티브 자바
- 프로그래머스
- 백준
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |