XStream은 Java Object에 대한 xml serialize를 제공하는 Libraray입니다. 물론 deserialize도 지원하며, json 변환 역시 지원하지요.

문제상황

하지만 XStream은 Object의 property 값이 null 인 경우에는 해당 property를 serialize 하지 않습니다. 예를 들면…

@XStreamAlias("Person")
class Person {
	public String firstName;
	public String lastName;
	public String middleName;
}
......
Person jobs = new Person();
jobs.firstName = "Steve";
jobs.lastName = "Jobs";

XStream xstream = new XStream();
xstream.processAnnotations(Person.class);
String xml = xstream.toXML(jobs);
System.out.println(xml);

위와 같은 코드는 아래와 같은 XML을 출력하게 됩니다.


  Steve
  Jobs
  

해결방법(XML) : Converter 구현

null value serialize를 위해서 여러가지 접근 방법이 있겠지만, Converter를 구현해서 사용하는게 가장 심플합니다.
아래코드는 null value를 빈 element로 변환하도록 하는 Converter 구현입니다.

/**
 * NullConverterHack.
 *
 *


 * {@link XStream}으로 serialize 할때 기본적으로 null value를 skip처리하므로,
 * 빈 엘리먼트를 삽입하도록 하는 {@link Converter} 클래스임.
 * 

 *


 * 아래와 같은 형태로 사용한다. 반드시 LOW PRIORITY로 사용해야 함.
 * 
 *  xstream.registerConverter(new NullConverterHack(xstream.getMapper()), XStream.PRIORITY_LOW);
 * 
 * 

 *
 * (주의) unmarshal을 지원하지 않는다.
 *
 * @see JsonWriterHack Json으로 serialize 할 경우에는 {@link NullConverterHack}과 {@link JsonWriterHack}을 함께 사용한다.
 * @author oddpoet
 */
public class NullConverterHack implements Converter {
	private Mapper mapper;

	public NullConverterHack(Mapper mapper) {
		this.mapper = mapper;
	}

	/**
	 * 모든 클래스에 대해서 작동.
	 *
	 * @param type
	 * @return
	 * @see com.thoughtworks.xstream.converters.ConverterMatcher#canConvert(java.lang.Class)
	 */
    @SuppressWarnings("unchecked")
	public boolean canConvert(Class type) {
        return true;
    }

    /**
     * marshalling.
     *
     * @param source
     * @param writer
     * @param context
     * @see com.thoughtworks.xstream.converters.Converter#marshal(java.lang.Object, com.thoughtworks.xstream.io.HierarchicalStreamWriter, com.thoughtworks.xstream.converters.MarshallingContext)
     */
    public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
    	Field [] fieldSet = source.getClass().getDeclaredFields();

		for (Field field : fieldSet) {
    		Object member;

			if (!mapper.shouldSerializeMember(source.getClass(), field.getName())) {
    			continue;
			}

    		field.setAccessible(true);
			try {
				member = field.get(source);
				String name = mapper.serializedMember(field.getDeclaringClass(), field.getName());

				if (member == null) {
	    			writer.startNode(name);
	    			// 값이 null이면 내용은 채우지 않고 startNode(), endNode()만 호출
	    			writer.endNode();
	    		} else {
	    			ExtendedHierarchicalStreamWriterHelper.startNode(writer, name, member.getClass());
	    			context.convertAnother(member);
	    			writer.endNode();
	    		}
			} catch (IllegalArgumentException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}

    	}
    }

    /**
     * unmarshal을 지원하지 않는다.
     *
     * @param reader
     * @param context
     * @return
     * @see com.thoughtworks.xstream.converters.Converter#unmarshal(com.thoughtworks.xstream.io.HierarchicalStreamReader, com.thoughtworks.xstream.converters.UnmarshallingContext)
     */
    public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
        return null;
    }
}

Converter 구현 코드가 길지 않으니 자세한 설명은 생략하겠습니다. 실제 사용방법은 아래와 같습니다.

Person jobs = new Person();
jobs.firstName = "Steve";
jobs.lastName = "";
jobs.middleName = null;

XStream xstream = new XStream();
// Converter를 등록한다. 단, PRIORITY_LOW로 등록하도록 한다.
xstream.registerConverter(new NullConverterHack(xstream.getMapper()), XStream.PRIORITY_LOW);

xstream.processAnnotations(Person.class);
String xml = xstream.toXML(jobs);
System.out.println(xml);

위와 같은 방법으로 객체를 XML로 serialize 하면 아래와 같은 결과를 얻을 수 있습니다. 공백문자와 null이 다른 형식으로 표현되고 있음에 유의하세요.


  Steve
   
   

JSON의 경우

위와 같이 Converter를 추가해서 사용할 경우 XML 변환시 null 값에 대한 빈 element 출력은 가능합니다만, JSON 출력의 경우 아래와 같이 null 이 아닌 빈 객체({})로 출력되는 문제가 있습니다.

Person jobs = new Person();
jobs.firstName = "Steve";
jobs.lastName = "";
jobs.middleName = null;

XStream xstream = new XStream(new JsonHierarchicalStreamDriver() {

	@Override
	public HierarchicalStreamWriter createWriter(Writer out) {
		return new JsonWriter(out, JsonWriter.DROP_ROOT_MODE | JsonWriter.STRICT_MODE);
	}

});
xstream.registerConverter(new NullConverterHack(xstream.getMapper()), XStream.PRIORITY_LOW);
xstream.processAnnotations(Person.class);
String xml = xstream.toXML(jobs);
System.out.println(xml);

즉 위와 같은 코드는 아래와 같은 JSON을 출력하게됩니다. null 대신 {}이 출력되지요.

{
  "firstName": "Steve",
  "lastName": "",
  "middleName": {}
}

Conveter의 Interface를 보면 알 수 있겠지만, JSON 출력시의 null 문제를 풀기 위해서는 Conveter 레벨에서 처리가 안됩니다. 결국 Writer 레벨에서 null 처리를 해야합니다. 그런데 기존 JsonWriter를 상속받아서 해결할 수 없기 때문에, 깔끔하지는 않지만 기존 JsonWriter 소스를 복사해서 새로운 클래스를 만들고 관련된 부분을 수정합니다. 수정해야할 부분은 endNode() 함수입니다. (아래 소스 참고)

/**
 * JsonWriterHack.
 *
 *


 * XStream 패키지에 있는 {@link JsonWriter}에 수정을 가한 클래스이며, (상속받아서 해결 불가능함)
 * {@link NullConverterHack}과 함께 사용할 경우, null 값을 Json의 null로 변환해준다. 당연히 공백문자열("")과는 구분된다.
 * 

 *


 * 수정된 부분은 {@link #endNode()} 함수이며, 수정한 부분에 comment를 달아놓았다.
 * 하지만 변경된 JsonWriter에 의해 사용시 예기치 못한 JSON 변환이 이루어질 수 있으므로, 주의하도록한다.
 * 

 *
 * @see NullConverterHack {@link NullConverterHack}과 함께 사용해야 한다.
 * @author oddpoet
 */
public class JsonWriterHack implements ExtendedHierarchicalStreamWriter {
	// ... 나머지 코드는 JsonWriter와 동일하게 그대로 두고, endNode() 함수만 수정한다.
	public void endNode() {
		depth--;
		Node node = (Node)elementStack.pop();

		if (node.clazz != null && node.isCollection) {
			if (node.fieldAlready) {
				readyForNewLine = true;
			}

			finishTag();
			writer.write("]");
		} else if (tagIsEmpty) {
			readyForNewLine = false;
			writer.write("null");  // <- 이부분을 수정. 원래는 writer.write("{}"); 였음.
			finishTag();
		} else {
			finishTag();

			if (node.fieldAlready) {
				writer.write("}");
			}
		}

		readyForNewLine = true;

		if (depth == 0 && ((mode & DROP_ROOT_MODE) == 0 || (depth > 0 && !node.isCollection))) {
			writer.write("}");
			writer.flush();
		}
	}
	// 나머지 코드는 그대로 유지...
}

자 그럼, JsonWriter 대신 위에서 만든 JsonWriterHack으로 대체하면 원하는 null 출력을 볼 수 있습니다.

Person jobs = new Person();
jobs.firstName = "Steve";
jobs.lastName = "";
jobs.middleName = null;

XStream xstream = new XStream(new JsonHierarchicalStreamDriver() {

	@Override
	public HierarchicalStreamWriter createWriter(Writer out) {
		return new JsonWriterHack(out, JsonWriterHack.DROP_ROOT_MODE | JsonWriterHack.STRICT_MODE);
	}

});
xstream.registerConverter(new NullConverterHack(xstream.getMapper()), XStream.PRIORITY_LOW);
xstream.processAnnotations(Person.class);
String xml = xstream.toXML(jobs);
System.out.println(xml);
{
  "firstName": "Steve",
  "lastName": "",
  "middleName": null
}

Json에서의 null serialize 의 경우, 기존 구현체를 Rewrite해야해서 그다지 깔끔한 방법은 아닙니다. XStream으로 JSON 출력제어를 하는데 제약이 있으므로, 다른 object – JSON 매핑 library를 사용하는게 좋을 수도 있습니다.

, , ,
Trackback

only 1 comment untill now

  1. highfive @ 2011-05-18 16:40

    제가 딱 찾던거네요.. 도움 많이 되었습니다.

Add your comment now

WordPress SEO