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
,

스프링 동적리스트 바인딩의 경우 최대 256개까지가 기본설정으로 되어 있다.

만약, 256개 이상을 파라미터로 넘기게 된다면 IndexOutOfBoundsException이 발생하게 될 것이다.


해결방법은 스프링 InitBinder의 기본설정 값을 변경해준다.

(LazyList를 이용하여 256개를 확장하는 방법도 있는 것 같은데, 이 방법은 안써봐서 패스~)


@Controller
public class TestController {
	private static final int AUTO_GROW_COLLECTION_LIMIT = 500;

	@InitBinder
	public void initBinder(WebDataBinder dataBinder) {
		dataBinder.setAutoGrowCollectionLimit(AUTO_GROW_COLLECTION_LIMIT);
	}
}


그리고 톰캣쪽 maxParameterCount값도 한번 확인해 보는 것이 좋다.

필자는 최대 500개의 리스트를 바인딩하기 위해 위와 같은 설정을 하였는데, 변경하고도 계속 다음과 같은 오류가 발생했다.


경고: More than the maximum number of request parameters (GET plus POST) for a single request ([10,000]) were detected. Any parameters beyond this limit have been ignored. To change this limit, set the maxParameterCount attribute on the Connector.


톰캣은 maxParameterCount값이 기본으로 10000개가 설정되어 있는데, 이 이상이 초과가 되면  에러가 발생함.

그래서 아래와 같이 -1 무제한으로 변경.

정확한 최대 파라미터 갯수가 측정될 경우엔 숫자를 써주는 것이 더 좋을듯하다.


<Connector port="8080" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               redirectPort="8443"
               URIEncoding="UTF-8"
               maxParameterCount="-1"/>



Posted by SungHoon, Park
,