태그 : 번역

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 (1/4)

마틴 파울러(Martin Fowler)

자바 커뮤니티에서는 줄곧 경량 컨테이너들이 경쟁적으로 출시 되고 있습니다. 이들은 다른 프로젝트에서 가지고 온 S/W 부품을 조립해서 하나로 통합된 응용 S/W를 만드는 데 도움이 됩니다. 이 컨테이너들은 S/W 부품들을 연결하는 방법에 대한 공통 패턴을 근간으로 하고 있습니다. 이 개념을 흔히 제어 역전(Inversion of Control)이라고 부릅니다. 이 글에서는 이 패턴을 의존성 주입(Dependency Injection)과 서비스 위치탐색기(Service Locator)로 세분화하고 이들을 비교함으로 이 패턴의 작동 방식을 파고들어보려고 합니다. 이 두 가지 방식 중 어떤 것을 선택할지 논하는 것보다 S/W 부품를 이용하는 코드에서 설정을 분리하자는 원칙이 더 중요합니다.

주류 J2EE의 대안을 만들려는 엄청난 규모의 활동은 기업용 자바 세상에서 흥미있는 일 중 하나입니다.그 중 많은 수가 오픈 소스에서 일어나고 있습니다. 상당수는 무겁고 복잡한 주류 J2EE 세상에 대한 반발이지만 다수는 대안을 찾으려는 탐구와 창의적인 시도이기도 합니다. 어떻게 각기 다른 요소를 함께 연결할 것인지를 다루는 것, 예를 들어 서로를 잘 알지 못하는 여러 팀에서 각자 만든 웹 컨트롤러 아키덱처와 데이터베이스 인터페이스를 어떻게 하나로 묶을 것인지가 이것들이 해결하려는 공통 사안입니다. 다수의 프레임워크가 이 문제를 해결했고 그중 몇몇은 다른 계층의 컴포넌트들을 조립하는 일반적인 기능까지 제공하는데 이르렀습니다. 이들을 종종 경량 컨테이너라고 부르는데 피코 컨테이너와 스프링을 예로 들 수 있습니다.

이 컨테이너들은 인상적인 몇가지 설계 원칙에 기초하고 있습니다. 이 원칙들은 특정 컨테이너는 물론 자바라는 플렛폼까지 넘어서 적용될 수 있습니다. 여기서 이 원칙 중 몇가지를 탐구해보려 합니다. 예로 사용한 언어는 자바이지만 내가 썼던 다른 원칙들 대부분이 그런 것처럼 다른 객체지향 환경(특히 .Net)에도 적용할 수 있습니다.

S/W 구성 요소들을 하나로 묶는 주제를 다루려면 서비스와 컴포넌트라는 두 단어를 둘러싼 껄끄러운 용어 문제 먼저 해결해야 할 것 같습니다. 이 둘의 정의와 관련된 길고 모호한 글들을 쉽게 찾을 수 있습니다. 저는 여기서 제가 이 단어를 어떤 의미로 사용하는지 말하려고 합니다.

전 컴포넌트를 재사용되는 소프트웨어 덩어리를 의미할 때 사용합니다. 컴포넌트는 컴포넌트 제작자가 어떤 영향도 끼칠 수 없는 응용 소프트웨어에 변경 없이 재사용 할 수 있습니다. 여기서 '변경 없이'라고 한 건 응용 소프트웨어가 컴포넌트 제작자가 허용한 방법으로 컴포넌트를 확장해서 행위를 바꾸어 사용할 수는 있을지언정  컴포넌트의 소스 코드를 고치지는 않는다는 것을 뜻합니다.

서비스는 외래 애플리케이션이 사용한다는 측면에서는 컴포넌트와 비슷합니다. 주요 차이점이라면 컴포넌트가 (jar 파일, 조립, DLL, 소스코드 삽입등 같이) 지역적으로 사용될 것이라 보는 반면에 서비스는 동기 방식이든 (웹 서비스, 전문 시스템, RPC, 소켓 등) 비동기 방식이든 모종의 인터페이스를 사용해 원격에서 사용 되리라는 것입니다.

이 글에서는 대부분 서비스라고 쓰겠지만 상당수의 논리는 지역적 컴포넌트에도 똑같이 적용될 수 있습니다. 사실 원격 서비스를 쉽게 사용하려면 흔히 로컬 컴포넌트 프레임워크 같은 것이 필요합니다. 컴포넌트냐 서비스냐에 대해 논하는 글은 읽는 것도 쓰는 것도 피곤한 일입니다. 그리고 요즘은 서비스가 훨씬 더 인기있습니다.

순진한 예제

모든 것을 분명하게 하고자 실행 가능한 예제 하나를 사용하려 합니다. 제가 흔히 쓰는 예제가 그렇듯 이 예제도 엄청나게 단순합니다.  비현실적으로 보일 만큼 작지만 복잡한 실제 예의 수렁에 빠지는 일 없이 뭐가 어찌 돌아가는지 눈으로 확인할 수 있는 정도는 되리라 생각합니다.

예제는 특정 감독이 연출한 영화의 목록을 찾아주는 컴포넌트 입니다. 이 놀랍도록 유용한 기능이 메소드 하나로 구현되어 있습니다.

class MovieLister...
    public Movie[] moviesDirectedBy(String arg) {
        List allMovies = finder.findAll();
        for (Iterator it = allMovies.iterator(); it.hasNext();) {
            Movie movie = (Movie) it.next();
            if (!movie.getDirector().equals(arg)) it.remove();
        }

        return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
    }

기능 구현이 극단적으로 단순합니다. 보유하고 있는 모든 필름 정보를 달라고 (조금 있다 얻게 될) 찾기 객체에게 요청합니다. 그리고 목록을 뒤쳐 반환할 특정 감독의 작품을 뒤집니다. 이 순진한 코드는 단지 이 글에서 진짜 다루려고 하는 내용을 위한 골격에 지나지 않으므로 손대지 않을 겁니다.

이 글에서 진짜로 관심 있는 것은 찾기 객체, 특히 MovieLister와 특정 찾기 객체를 연결하는 방법입니다. 이것이 흥미있는 이유는 이 멋진 movieDirectedBy 메소드가 영화를 보관하는 방식과 전혀 상관 없이 움직였으면 하기 때문입니다. 그래서 메소드는 단지 찾기 객체를 참조하기만 하면 되고 그 파인더는 findAll 메소드 호출에 어떻게 응답해야하는지 알기만 해도 되었으면 합니다. 찾기 객체의 인터페이스를 정의하는 것으로 이것을 실현할 수 있습니다.

public interface MovieFinder {
    List findAll();
}

이제 이렇게 해서 결합이 잘 분리되었습니다. 하지만 어느 지점에선가 실제 영화 목록을 얻으려면 구상 클래스를 얻어야 합니다. 한번 생성자에 이 코드를 넣어보겠습니다.

class MovieLister...
  private MovieFinder finder;
  public MovieLister() {
    finder = new ColonDelimitedMovieFinder("movies1.txt");
  }

구현 클래스의 이름은 콜론으로 구분되어 있는 텍스트 파일에서 목록을 얻으려고 한다는 뜻입니다. 단지 어떤 구현체가 있다는 사실만이 중요하므로 상세한 부분은 생략하겠습니다.

이 클래스를 혼자서만 쓴다면 모든 것이 훌륭하고 멋집니다. 그런데 친구들이 이 멋진 기능을 쓰고 싶은 열망에 압도되어 이 프로그램를 복사해달라고 할 때 어떤 일이 일어날까요? 그 친구들도 "movies1.txt"라는 파일에 콜론으로 구별된 형태로 영화 목록을 보관하고 있다면 모든 것이 환상적이겠죠. 만약 영화 목록 파일의 이름이 다르다면 설정 파일에 파일 이름을 넣도록 해서 쉽게 해결할 수 있습니다. 그런데 만약 완전히 다른 형태로 영화 목록을 저장하고 있다면 어떨까요? SQL 데이터베이스나 XML이나 웹 서비스나 형식이 다른 텍스트 파일이면 말입니다. 이 경우에는 다른 클래스로 데이터를 얻어 와야 합니다. 이미 MovieFinder 인터페이스를 정의 했기 때문에 moviesDirectedBy 메소드를 바꾸지 않아도 됩니다. 그렇지만 여전히 올바른 finder 구현물의 인스턴스를 얻어 제자리에 두는 어떤 방법이 필요합니다.

그림 1 : 목록 클래스에서 단순히 생성했을 때 의존성 관계

그림 1에서 지금의 의존 관계를 볼 수 있습니다. MovieLister 클래스는 MovieFinder 인터페이스와 그 구현물에 종속되어 있습니다. 인터페이스에만 종속되어 있으면 좋겠는데 어떻게하면 인스턴스를 가져와 쓸 수 있을까요?

P of EAA에는 이런 상황을 Plugin 패턴으로 설명하고 있습니다. 컴파일 시점에는 찾기 객체의 구현 클래스가 프로그램에 연결되어 있지 않습니다. 친구들이 어떤 것을   쓰려는지 모르기 때문입니다. 대신 목록 객체가 어떤 구현물과도 작동되고 나중에 언젠가 그 구현물을 별 조작없이 장착할 수 있으면 좋겠습니다. 문제는 어떻게 우리의 목록 객체가 구현 클래스에 대해 잘 모르면서도 여전히 인스턴스와 협력하여 해야 할일을 할 수 있도록 하느냐는 것입니다.

이것을 실제 시스템으로 확대해서 보겠습니다. 실제 시스템에서는 이런 서비스와 컴포넌트가 수십개나 있을 것입니다. 이 컴포넌트들의 사용하는 곳마다 인터페이스를 통해 호출하도록 추상화 할 수 있습니다. 만약 컴포넌트가 그 인터페이스에 맞지 않다면 어댑터를 써야겠죠. 하지만 만약 이 시스템을 여러가지 다른 방식으로 배포하기 원한다면 이 서비스들과의 상호작용할 수 있도록 처리하기 위해 플러그인을 사용해야 합니다. 그러면 여러 다른 구현물은 여러 다른 배포 환경에서 사용할 수 있습니다.

이제 핵심 문제는 이 여러 플러그인들을 어떻게 조립해서 한 애플리케이션으로 만들 수 있느냐는 것입니다. 이것이 새로운 경량 컨테이너들이 해결하려고 하는 주된 문제 영역 중 하나입니다. 그리고 보편적으로 제어 역전을 그 해법으로 사용합니다.

제어 역전


경량 컨테이너들을 소개할 때에 왜 그것들이 유용한지를 말하면서 이유가 "제어 역전"을 구현했기 때문이라고 할 때면 전 무척 당황스럽습니다. "제어 역전"은 프레임워크들이 갖는 공통적인 특성입니다. 그렇기에 경량 컨테이너들이 자기들은 제어 역전을 쓰기 때문에 특별하다고 말하는 건 마치 내 차에는 바퀴가 있어서 특별하다고 하는 것과 같습니다.

질문해야 할 것은 어떤 제어를 역전하려고 하느냐는 것입니다. 제가 처음 접했던 제어 역전은 유저 인터페이스의 중심 제어에서였습니다. 초기 유저 인터페이스들은 애플리케이션 프로그램에서 제어를 했습니다. 흔히 "이름을 입력하시오", "주소를 입력하시오" 같은 일련의 명령 문구를 가지고 있고 프로그램이 이것들을 하나씩 물어보고는 답을 입력 받곤 했습니다. 그래픽(몇몇 스크린 기반도) 유저 인터페이스에서는 유저 인터페이스 프레임워크가 제어권을 가지고 있고 프로그램은 화면의 각 요소에 대한 이벤트 처리자만 마련하고 있으면 됩니다.  프로그램의 주 제어권은 역전된 것입니다. 애플리케이션에서 프레임워크로 옮겨진 것이지요.

이 새로운 컨테이너들에서의 역전은 플러그인 구현물을 찾을 방법과 관련되어 있습니다. 우리의 순진한 예제에서 목록 객체는 찾기 구현물의 인스턴스를 직접 생성해서 씁니다. 이렇게 하면 찾기 객체를 플러그인이라고 할 수 없습니다. 경량 컨테이너들이 사용하는 접근법은 플러그인을 사용하는 누구라도 어떤 관례를 따르기만 하면 독립된 조립 모듈이 구현물을 목록 객체에 주입하도록 보장합니다.

결국 이런 패턴을 지칭하는 보다 지협적인 이름이 필요한 것 같습니다. 제어 역전은 너무 일반적인 용어라서 사람들이 혼란스러워 합니다. 다양한 IoC 지지자들과 많은 토론을 한 결과 의존성 주입이라고 부르기로 합의하였습니다.

이제 여러가지 형태의 의존성 주입에 대해서 말하려고 하지만 여기서 확실해 해두고 싶은 것은 이것이 플러그인 구현물에서 애플리케이션 클래스의 의존성을 제거하는 유일한 방법이 아니라는 것입니다. 의존성 제거에 쓸 수 있는 다른 패턴으로 서비스 위치탐색기가 있습니다. 이에 대해서는 의존성 주입을 설명하고 나서 다루겠습니다.

[번역] 제어 역전 컨테이너와 의존성 주입 패턴 [1][2][3][4]

---------

워낙 유명한 글이지만 막상 이 글을 읽는 사람도 IOC나 DI의 의미를 이해하고 쓰는 사람이 많지 않은 것 같습니다. 스프링을 쓰는 몇몇 프로젝트에 참여해서 일해본 적이 있지만 매번 스프링을 이런식으로 쓰는 게 무슨 의미인가? 하는 회의만 들었습니다. 그래서 출퇴근 시간에 조금씩 번역해서 올려보려고 합니다. 대충 보니 네 부분으로 나눌 수 있을 것 같습니다. 

by 박성철 | 2009/09/28 00:37 | 프로그래밍 이야기 | 트랙백 | 덧글(0)

[번역] 도메인 로직과 SQL

원본 글은 http://martinfowler.com/articles/dblogic.html 에 있습니다.

최종 주요 갱신: 2003년 2월

우리는 지난 20여 년 이상 데이터베이스 지향 소프트웨어 개발자와 메모리에서 처리하는 애플리케이션 소프트웨어 개발자 간의 간격이 커지는 것을 봐왔습니다. 이 때문에 SQL이나 저장 프로시듀어 같은 데이터베 이스의 기능을 어떻게 이용할지에 대한 논의가 생겨났습니다. 이 글에서 전 업무 로직을 SQL 질의문 안에 둘 것인지 아니면 메모리에 있는 코드 안에 둘 것인지에 대한 문제를 주로 성능과 유지보수성의 관점에서 단순한 -그러나 SQL 충분히 활용한 - 예제를 사용해 살펴보려고 합니다.



이어지는 내용

by 박성철 | 2008/06/13 21:03 | 프로그래밍 이야기 | 트랙백(8) | 핑백(1) | 덧글(0)

[번역] 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)

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