[번역] XStream 배우기 : 변환기(Converter) 배우기

이글은 XStream 교재 중 "Converter Tutorial"을 번역한 것입니다.

단순한 변환기

준비하기



다음은 가장 기본적인 변환기입니다. 먼저 Person 객체로 시작하겠습니다.

  1. 2분만에 배우기
  2. 별칭(Alias) 배우기
  3. 애노테이션(Annotations) 배우기
  4. 변환기(Converter) 배우기
  5. 객체 스트림(Object Streams) 배우기
  6. 영속화 API(Persistence API) 배우기
  7. JSON 배우기
package com.thoughtworks.xstream.examples;

public class Person {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

이제 인스턴스를 만들고 XML로 변환해보겠습니다.
package com.thoughtworks.xstream.examples;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

public class PersonTest {

public static void main(String[] args) {
Person person = new Person();
person.setName("Guilherme");

XStream xStream = new XStream(new DomDriver());
System.out.println(xStream.toXML(person));
}
}
매우 보기 싫은 결과가 나옵니다. XML 코드에 패키지명을 포함한 전체 클래스 이름이 들어 있습니다.

<com.thoughtworks.xstream.examples.Person>
<name>Guilherme</name>
</com.thoughtworks.xstream.examples.Person>
이제 이 긴 클래스 이름을 보다 인간다운 뭔가(예를 들면 'person')로 바꾸는데 사용할 별칭(alias)를 만듭니다.

XStream xStream = new XStream(new DomDriver());
xStream.alias("person", Person.class);
System.out.println(xStream.toXML(person));
결과물이 한결 일기 쉽고 간결해졌습니다.

<person>
<name>Guilherme</name>
</person>
앞으로 가지고 놀 간단한 클래스를 구성했습니다. XStream 변환기가 어떤 일을 해주는지 보기로 하겠습니다.

PersonConverter 만들기

같이 만들어 볼 간단한 변환기는 이런 기능을 가지고 있습니다.
  1. Person 객체를 변환할 능력이 있는지 말함
  2. Person의 인스턴스를 XML로 변환
  3. XML을 새 Person 인스턴스로 변환
PersonConverter 클래스를 만들고 Converter 인터페이스를 구현하기 시작합니다.

package com.thoughtworks.xstream.examples;

import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class PersonConverter implements Converter {

public boolean canConvert(Class clazz) {
return false;
}

public void marshal(Object value, HierarchicalStreamWriter writer,
MarshallingContext context) {
}

public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
return null;
}
}
이제 호출하면 Person 객체만 다룰 수 있다는 것을 얘기해줍니다. Person 외에는 전혀 다룰 수 없습니다. Person을 상속한 클래스를 포함해서......

public boolean canConvert(Class clazz) {
return clazz.equals(Person.class);
}
generic 변환기를 작업하지 않는이상 두번째 단계는 분명합니다.
marshal() 메소드는 객체를 XML로 변환하는 책임을 집니다. 이 메소드는 인수 세개를 받습니다.
  1. 변환하려고 하는 객체
  2. 데이터를 출력할 Writer
  3. 현재의 MarshallingContext
객체를 Person으로 캐스팅을 먼저 합니다.
Person person = (Person) value;
이제 데이터를 출력할 수 있습니다. 'fullname'이라는 노드를 만드는 것으로 시작해서 person 객체의 name 값을 추가합니다.

writer.startNode("fullname");
writer.setValue(person.getName());
writer.endNode();
간단하죠?

public void marshal(Object value, HierarchicalStreamWriter writer,
MarshallingContext context) {
Person person = (Person) value;
writer.startNode("fullname");
writer.setValue(person.getName());
writer.endNode();
}
startNode()와 endNode()는 원하는 만큼 여러번 호출할 수 있습니다. 하지만 열어 놓은 것들은 모두 닫아야 한다는 것을 잊지 마십시오.  그리고  변환 작업은 대부분 setValue() 메소드를 호출하는 부분에서 합니다.

unmarshal()로 넘어가보겠습니다. 트리 구조의 자료를 옮겨다니기 위해 moveDown()과 moveUp() 메소드를 사용합니다. 이렇게 단순히 moveDown(), 읽기, moveUp()를 합니다.

                Person person = new Person();
reader.moveDown();
person.setName(reader.getValue());
reader.moveUp();
다음과 같은 변환기가 주어졌습니다.

package com.thoughtworks.xstream.examples;

import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class PersonConverter implements Converter {

public boolean canConvert(Class clazz) {
return clazz.equals(Person.class);
}

public void marshal(Object value, HierarchicalStreamWriter writer,
MarshallingContext context) {
Person person = (Person) value;
writer.startNode("fullname");
writer.setValue(person.getName());
writer.endNode();
}

public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
Person person = new Person();
reader.moveDown();
person.setName(reader.getValue());
reader.moveUp();
return person;
}
}
이 변환기를 등록하보죠. 우리 프로그램의 main() 메소드는 보는 것 같습니다.

package com.thoughtworks.xstream.examples;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

public class PersonTest {

public static void main(String[] args) {
Person person = new Person();
person.setName("Guilherme");

XStream xStream = new XStream(new DomDriver());
xStream.registerConverter(new PersonConverter());
xStream.alias("person", Person.class);
System.out.println(xStream.toXML(person));
}

}
어떻게 변환기를 등록하는지 보셨나요? 단순히 registerConver()를 호출하면 됩니다.
xStream.registerConverter(new PersonConverter());
최종 결과는 이렇습니다.
<person>
<fullname>Guilherme</fullname>
</person>
아마 이렇게 말할겁니다. "tree만 바뀌었잖아! 나는 데이터를 변환하고 싶어!" 라고......

새 자식 노드를 만드는 대신 person 테그에 fullname이라는 속성을 사용하도록 시도해보세요.

타입을 문자열 표현으로 대체하기

Person 문자열로 표현할 수 있게 향상시켜보겠습니다. 이 문자열은 인스턴스를 다시 만들 때에 필요한 모든 텍스트를 포함하고 있어야 합니다.
package com.thoughtworks.xstream.examples;

public class Person {

private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String toString() {
return getName();
}
}
이 번 경우에는 변환기를 이렇게 단순화 할 수 있습니다.

package com.thoughtworks.xstream.examples;

import com.thoughtworks.xstream.converters.basic.AbstractSingleValueConverter;

public class PersonConverter extends AbstractSingleValueConverter {

public boolean canConvert(Class clazz) {
return clazz.equals(Person.class);
}

public Object fromString(String str) {
Person person = new Person();
person.setName(string);
return person;
}
}
우리 XML이 더 깔끔해졌을 뿐 아니라 Person 클래스의 별칭(alias) 덕분에 단순히지기까지 했습니다. 문자열 표현이 완성되었으니 이제 내포된 요소(fullname 요소)는 더 이상 필요 없습니다. 
<person>Guilherme</person>
날짜 변환기

이제  Converter 인터페이스가 어떻게 작동하는지 알았으니 정보 변환에 로케일을 사용하는 단순한 달력 변환기를 만들어 보겠습니다.

우리 변환기는 생성자가 Locale을 받아서 멤버 변수에 보관할 것입니다.

 
package com.thoughtworks.xstream.examples;

import java.util.Locale;

import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class DateConverter implements Converter {

private Locale locale;

public DateConverter(Locale locale) {
super();
this.locale = locale;
}

public boolean canConvert(Class clazz) {
return false;
}

public void marshal(Object value, HierarchicalStreamWriter writer,
MarshallingContext context) {
}

public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
return null;
}
}
이제 Calendar를 상속한 모든 객체를 변환 하도록 하겠습니다. 이는 어떤 클래스가 Calendar 클래스에 대응이 될 수 있다면 그 클래스는 추상 클래스 Calendar를 상속했다는 뜻입니다.

public boolean canConvert(Class clazz) {
return Calendar.class.isAssignableFrom(clazz);
}
Calendar를 지역화된 문자열로 변화해보겠습니다.  먼저 객체를 Calendar로 캐스팅 하고 Date를 추출한 다음 지역화된 문자열로 변화하는 날짜 변환기를 얻기 위해 DateFormat 팩토리 메소드를 사용하겠습니다.

public void marshal(Object value, HierarchicalStreamWriter writer,
MarshallingContext context) {

Calendar calendar = (Calendar) value;

// grabs the date
Date date = calendar.getTime();

// grabs the formatter
DateFormat formatter = DateFormat.getDateInstance(DateFormat.FULL,
this.locale);

// formats and sets the value
writer.setValue(formatter.format(date));
}
그리고 이번에는 반대로 복원하기 위해서 GregorianCaldendar를 만들고 지역화된 DateFormat 인스턴스를 얻은 다음 문자열을 파싱해서 Date로 만들고 이 날짜를 원래의 GregorianCaldendar에 넣도록 합니다.
public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {

// creates the calendar
GregorianCalendar calendar = new GregorianCalendar();

// grabs the converter
DateFormat formatter = DateFormat.getDateInstance(DateFormat.FULL,
this.locale);

// parses the string and sets the time
try {
calendar.setTime(formatter.parse(reader.getValue()));
} catch (ParseException e) {
throw new ConversionException(e.getMessage(), e);
}

// returns the new object
return calendar;
}
Note 1 : 어떤 DateFormat 구현체는 쓰레드에서 안전하지 않다는 것을 기억하십시오. 따라서,  이 포멧터를 변환기의 멤버 변수로 사용해서는 안됩니다.
Note 2 : 이 구현체는 읽기나 저장 이후에 다른 유형의 카렌다를 GregorianCalendar로 변환합니다. 만약 이 방식을 원하지 않는다면 canConvert() 메소드에서 주어진 class가 GregorianCalendar일 때에만 true를 반환하게 수정하십시오.

이제 변환기를 완성했습니다.

package com.thoughtworks.xstream.examples;

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;

import com.thoughtworks.xstream.converters.ConversionException;
import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class DateConverter implements Converter {

private Locale locale;

public DateConverter(Locale locale) {
super();
this.locale = locale;
}

public boolean canConvert(Class clazz) {
return Calendar.class.isAssignableFrom(clazz);
}

public void marshal(Object value, HierarchicalStreamWriter writer,
MarshallingContext context) {
Calendar calendar = (Calendar) value;
Date date = calendar.getTime();
DateFormat formatter = DateFormat.getDateInstance(DateFormat.FULL,
this.locale);
writer.setValue(formatter.format(date));
  1. }

public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
GregorianCalendar calendar = new GregorianCalendar();
DateFormat formatter = DateFormat.getDateInstance(DateFormat.FULL,
this.locale);
try {
calendar.setTime(formatter.parse(reader.getValue()));
} catch (ParseException e) {
throw new ConversionException(e.getMessage(), e);
}
return calendar;
}
}
확인을 해보겠습니다. main 메소드를 갖는 DateTest를 만듭니다.
  1. 현재시간으로 Calendar를 만든다.
  2. XStream 객체를 만든다.
  3. 변환기를 대한민국 로케일로 등록한다.
  4. 객체를 XML로 변환한다.
자... 이미 이런 작업을 어떻게 해야하는지 알고 있으니 설명 없이 진행하겠습니다.

package com.thoughtworks.xstream.examples;

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;

public class DateTest {

public static void main(String[] args) {

// grabs the current date from the virtual machine
Calendar calendar = new GregorianCalendar();

// creates the xstream
XStream xStream = new XStream(new DomDriver());

// brazilian portuguese locale
xStream.registerConverter(new DateConverter(new Locale("ko", "KR")));

// prints the result
System.out.println(xStream.toXML(calendar));

}
}
결과는 로케일에 따라 다르겠지만 이런 형식입니다.
<gregorian-calendar>2006년 2월 10일 금요일</gregorian-calendar>
Note : 위에서 별칭을 따로 주지 않았지만 GregorianCalendar의 기본 별칭인 gregorian-calendar으로 표시되었습니다.

이제 위에 결과를 객체로 복원해보겠습니다.
// loads the calendar from the string
Calendar loaded = (Calendar) xStream
.fromXML("<gregorian-calendar>2006년 2월 10일 금요일</gregorian-calendar>");
그리고 이것을 시스템 로케일의 단축형 날짜 포멧으로 출력해보죠.
// prints using the system defined locale
System.out.println(DateFormat.getDateInstance(DateFormat.SHORT).format(
loaded.getTime()));
결과는 아마도 이렇게 나올 것입니다. (시스템 로케일이 미국 영어라면)
2/10/06
복잡한 변환기

지금까지 만든 객체들을 모아보겠습니다.
package com.thoughtworks.xstream.examples;

public class Birthday {

private Person person;
private Calendar date;

public Person getPerson() {
return person;
}

public void setPerson(Person person) {
this.person = person;
}

public Calendar getDate() {
return date;
}

public void setDate(Calendar date) {
this.date = date;
}
}
XStream은 이 객체를 아무런 문제없이 변환하는 능력을 가지고 있지만 시연을 위해서 직접 변환기를 만들도록 하겠습니다. 이미 만든 Person과 Calendar 변환기를 재사용하는 것이 좋겠습니다. canConvert 메소드는 명백하게 단순합니다. 파생된 객체는 추가 필드를 갖을 수 있기 때문에 여기에서는 변환하지 않겠습니다. 그러나 우리의 멤버 필드와 널 값을 변환하기 위해 XStream이 이미 등록되어 있는 변환기들은 사용하도록 하겠습니다.

package com.thoughtworks.xstream.examples;

import java.util.Calendar;

import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class BirthdayConverter implements Converter {

public boolean canConvert(Class clazz) {
return Birthday.class == clazz;
}

public void marshal(Object value, HierarchicalStreamWriter writer,
MarshallingContext context) {
Birthday birthday = (Birthday)value;
if (value.getPerson() != null) {
writer.startNode("person");
context.convertAnother(value.getPerson());
writer.endNode();
}
if (value.getDate() != null) {
writer.startNode("birth");
context.convertAnother(value.getDate());
writer.endNode();
}
}

public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
Birthday birthday = new Birthday();
while (reader.hasMoreChildren()) {
reader.moveDown();
if ("person".equals(reader.getNodeName())) {
Person person = (Person)context.convertAnother(birthday, Person.class);
birthday.setPerson(person);
} else if ("birth".equals(reader.getNodeName())) {
Calendar date = (Calendar)context.convertAnother(birthday, Calendar.class);
birthday.setDate(date);
}
reader.moveUp();
}
return birthday;
}
}
만약 Birthday 구현체의 필드가 확실히 null 값을 갖지 않는다면 변환과 복원 과정에서 null 조건을 제거하고 반복문과 테그 비교문을 생략할 수 있습니다.

package com.thoughtworks.xstream.examples;

import java.util.Calendar;

import com.thoughtworks.xstream.converters.Converter;
import com.thoughtworks.xstream.converters.MarshallingContext;
import com.thoughtworks.xstream.converters.UnmarshallingContext;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class BirthdayConverter implements Converter {

public boolean canConvert(Class clazz) {
return Birthday.class == clazz;
}

public void marshal(Object value, HierarchicalStreamWriter writer,
MarshallingContext context) {
Birthday birthday = (Birthday)value;
writer.startNode("person");
context.convertAnother(value.getPerson());
writer.endNode();
writer.startNode("birth");
context.convertAnother(value.getDate());
writer.endNode();
}

public Object unmarshal(HierarchicalStreamReader reader,
UnmarshallingContext context) {
Birthday birthday = new Birthday();
reader.moveDown();
Person person = (Person)context.convertAnother(birthday, Person.class);
birthday.setPerson(person);
reader.moveUp();
reader.moveDown();
Calendar date = (Calendar)context.convertAnother(birthday, Calendar.class);
birthday.setDate(date);
reader.moveUp();
return birthday;
}
}












by 박성철 | 2008/01/25 01:25 | 프로그래밍 이야기 | 트랙백 | 덧글(0)

트랙백 주소 : http://gyumee.egloos.com/tb/1334158
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]

:         :

:

비공개 덧글

◀ 이전 페이지다음 페이지 ▶