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속성이 같을 경우도 커버할 수 있어 문제를 해결할 수 있다. ^^;




Posted by SungHoon, Park
,