API 통신을 할 때, 결과값 데이터형식이 XML일 경우 스프링의 Jaxb2Marshaller를 주로 사용하였다.
applicationContext에 빈을 설정하고, org.springframework.oxm패키지내에 있는 Unmarshaller 인터페이스에 해당 빈을 주입하여 unmarshal() 메소드를 통해 언마샬링을 하였다.
문제는 언마샬링을 할 때 대상이 되는 JAXB 모델중 @XmlRootElement에 name 속성이 같은 경우 Unmarshaller에서 어떤 모델로 인식해야 되는지 몰라 오류가 발생하는 것이였다.
소스는 다음과 같다.
[applicationContext.xml]
<bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
<property name="packagesToScan">
<list>
<value>kr.co.pshcode.user.jaxb</value>
</list>
</property>
</bean>
[UserBOImpl.java]
import org.springframework.oxm.Unmarshaller;
@Slf4j
@Service
public class UserBOImpl implements UserBO {
@Autowired
private Unmarshaller unmarshaller;
private String getUser(String userId) {
User user = null;
try {
user = (User)unmarshaller.unmarshal(new StreamSource(makeUserApiUrl(userId, "PERSONAL")));
} catch (Exception e) {
log.error("user api error : {}\n", userId, e);
}
return user;
}
private String getGroupUser(String groupUserId) {
GroupUser groupUser = null;
try {
groupUser = (GroupUser)unmarshaller.unmarshal(new StreamSource(makeUserApiUrl(groupUserId, "GROUP")));
} catch (Exception e) {
log.error("group user api error : {}\n", groupUserId, e);
}
return groupUser;
}
}
[서비스API XML]
<!-- 일반유저 -->
<USER>
<COMMON></COMMON>
<DETAIL>
<ID></ID>
<NAME></NAME>
<EMAIL></EMAIL>
</DETAIL>
</USER>
<!-- 그룹유저 -->
<USER>
<COMMON></COMMON>
<DETAIL>
<GROUP_ID></GROUP_ID>
<GROUP_NAME></GROUP_NAME>
<GROUP_EMAIL></GROUP_EMAIL>
</DETAIL>
</USER>
[User.java, GroupUser.java]
@Data
@XmlRootElement(name = "USER")
@XmlAccessorType(XmlAccessType.FIELD)
public class User {
@XmlElement(name = "COMMON")
private UserCommon common;
@XmlElement(name = "DETAIL")
private UserDetail detail;
}
@Data
@XmlRootElement(name = "USER")
@XmlAccessorType(XmlAccessType.FIELD)
public class GroupUser {
@XmlElement(name = "COMMON")
private GroupUserCommon common;
@XmlElement(name = "DETAIL")
private GroupUserDetail detail;
}
사용자정보를 제공하는 API측에서는 XML의 루트를 <USER>로 일반유저, 그룹유저별로 <COMMON>과 <DETAIL>에 다른 엘리먼트명으로 각각 정보를 제공해주고 있다.
(비슷하지만 다른 느낌? 그런 느낌적인 느낌..)
이럴경우, Unmarshaller에서 정확한 JAXB 모델을 판단할 수 없기 때문에 언마샬링 오류가 발생하게 되는 것이다.
해결을 할 수 있는 간단한 방법으로는 2가지 케이스를 모두 언마샬링 할 수 있는 통합 모델을 만드는 것이다.
UserCommon + GroupUserCommon 합치기
UserDetail + GroupUserDetail 합치기
하지만 이렇게 처리할 경우 데이터를 객체지향적으로 관리할 수 없다.
파싱할 XML 정보가 늘어날때마다 하나의 모델에 계속 추가를 하게 되면 결국 어떤 API에서만 활용되는 정보인지 구분이 안가게 된다. 그래서 이 방법은 Pass!
다른 방법은 Jaxb2Marshaller를 상속받아서 Class를 선택적으로 넘겨받을 수 있는 메소드를 만들어 기능을 확장하는 것이다. (unmarshal() 메소드 오버로딩)
Castor나 Jibx 같은 다른 OXM은 사용 안해봐서 설명하는 방법과 비슷한 방법이 있는지는 잘 모르겠다. ㅎㅎ
암튼 개선한 소스는 다음과 같다.
[개선된 CustomJaxb2Marshaller.java]
public class CustomJaxb2Marshaller extends Jaxb2Marshaller {
public Object unmarshal(Source source, Class<?> clazz) throws JAXBException {
Unmarshaller unmarshaller = createUnmarshaller();
return unmarshaller.unmarshal(source, clazz).getValue();
}
}
Jaxb2Marshaller 클래스에 존재하는 createUnmarshaller() 메소드를 활용해서 javax.xml.bind 패키지의 Unmarshaller를 획득한 후, unmarshal(source, clazz) 메소드를 활용해서 선택적으로 파싱할 JAXB 모델 Class를 넘겨주어 언마샬링을 하는 것이다.
import kr.co.pshcode.oxm.CustomJaxb2Marshaller;
@Slf4j
@Service
public class UserBOImpl implements UserBO {
@Autowired
private CustomJaxb2Marshaller marshaller;
private String getUser(String userId) {
User user = null;
try {
user = (User)marshaller.unmarshal(new StreamSource(makeUserApiUrl(userId, "PERSONAL")), User.class);
} catch (Exception e) {
log.error("user api error : {}\n", userId, e);
}
return user;
}
private String getGroupUser(String groupUserId) {
GroupUser groupUser = null;
try {
groupUser = (GroupUser)marshaller.unmarshal(new StreamSource(makeUserApiUrl(groupUserId, "GROUP")), GroupUser.class);
} catch (Exception e) {
log.error("group user api error : {}\n", groupUserId, e);
}
return groupUser;
}
}
이렇게 처리할 경우, 기존의 Jaxb2Marshaller를 활용하면서 @XmlRootElement에 name속성이 같을 경우도 커버할 수 있어 문제를 해결할 수 있다. ^^;