Java에서의 XML 가공을 간소화 해주는 JDOM
대부분의 개발자는 과거 XML 데이타 구조를 가공하기 위해 수많은 Java 라이브러리 중 하나를 이용해본 적이 있을 것이다. 그렇다면 JDOM(Java Document Object Model)은 무엇이며 개발자는 왜 JDOM을 필요로 할까?
JDOM은 Java에 최적화된 XML 데이타 가공을 위한 개방 소스 라이브러리이다. JDOM은 W3C(World Wide Web Consortium) DOM과 유사하기는 하지만, DOM을 기반으로 설계되거나 DOM을 모델링하지 않은 대안적인 문서 객체 모델이다. 가장 큰 차이점은 DOM은 언어 중립적으로 설계되었고 초기에 HTML 페이지의 JavaScript 가공에 주로 이용되었던 반면, JDOM은 Java 전용으로 설계됐기 때문에 메소드 오버로딩(method overloading), 컬렉션(collections), 리플렉션(reflection) 및 친숙한 프로그래밍 환경 등 Java의 기본 기능들을 활용한다는 데 있다. Java 프로그래머에게는 JDOM이 보다 자연스럽고 ‘알맞게’ 느껴질 것이다. 이는 언어 중립적인 CORBA(Common Object Request Broker Architecture)에 비해 Java에 최적화된 RMI(Remote Method Invocation) 라이브러리가 보다 더 자연스럽게 느껴지는 것과 유사하다고 할 수 있다.
JDOM은 jdom.org에서 구할 수 있으며, 개방 소스로 Apache 스타일(상용 친화적) 라이선스로 제공된다. JDOM은 공동 협력을 통해 설계 및 개발됐으며, 메일링 리스트에 등록된 가입자만도 3,000여 명에 이른다. 또한 이 라이브러리는 Sun의 JCP(Java Community Process)에 Java Specification Request(JSR-102)로 채택됐으며, 곧 공식 Java 사양으로 채택될 것으로 전망된다.
이 글에서는 JDOM의 기술적 측면에 대해 다룰 것이다. 먼저, 주요 클래스에 대한 정보를 소개하고, 이어 Java 프로그램에서 JDOM을 이용하는 방법에 대해 설명할 것이다.
ORACLE XML TOOLS
이 XDK (XML Developer Kit)는 오라클이 개발자를 위해 제공하는 무료 XML 툴 라이브러리입니다. 이 라이브러리에서는 JDOM과 함께 이용할 수 있는 XML 파서와 XSLT 변환 엔진이 제공됩니다. 이들 툴에 대한 자세한 정보는 Oracle XML 홈페이지인 oracle.com/xml에서 제공됩니다.
파서를 다운로드받으려면 "XDK for Java"로 명명된 XML Developer Kit를 찾은 뒤 좌측의 "소프트웨어" 항목을 클릭해 다운로드를 시작합니다. 다운로드 받은 파일을 열면 xalparserv2.jar 파일에 파서가 들어있습니다.
JDOM 및 기타 소프트웨어가 기본값으로 오라클 파서를 이용하도록 구성하려면 oracle.xml.jax.JXSAXParserFactory에 JAXP javax.xml.parsers.SAXParserFactory 시스템 속성을 설정해야 합니다. 이는 JAXP에 오라클 파서를 이용하겠다고 밝히는 것입니다. 가장 쉬운 방법은 다음의 명령줄을 이용하는 것입니다.
java -Djavax.xml.parsers.SAXParserFactory=
oracle.xml.jaxp.JXSAXParserFactory
혹은 아래와 같이 프로그래밍 방식을 이용할 수도 있습니다.
System.setProperty("jaxax.xml.parsers
.SAXParserFactory",
"oracle.xml.jaxp.JXSAXParserFactory");
오라클은 XDK외에도 Oracle9i Database Release 2에 고유 XML 리포지토리를 제공하고 있습니다. Oracle9i XML Database (XDB)는 고성능을 지닌 고유 XML 스토리지 및 검색 기술입니다. XDB는 W3C XML 데이타 모델을 Oracle9i Database로 완벽하게 수용하며 XML의 네비게이션 및 질의를 위한 새로운 표준 액세스 방법을 제공합니다. XDB를 이용할 경우 관계형 데이타베이스 기술의 모든 이점과 함께 XML 기술의 장점을 활용할 수 있습니다. |
JDOM 패키지의 구조
JDOM 라이브러리는 6개 패키지로 구성되어 있다. 첫째, org.jdom 패키지에는 Attribute, CDATA, Comment, DocType, Document, Element, EntityRef, Namespace, ProcessingInstruction, Text 등 XML 문서와 그 컴포넌트를 나타내는 클래스들이 포함돼 있다. XML에 익숙한 개발자라면 클래스 이름만 봐도 이해가 될 것이다.
다음은 XML 문서를 생성하는 클래스를 담고 있는 org.jdom.input 패키지이다. 가장 중심적이고 중요한 클래스는 SAXBuilder이다. SAXBuilder는 수신되는 SAX(Simple API for XML) 이벤트를 참조해 이에 대응하는 문서를 구성함으로써 문서를 생성한다. 파일이나 다른 스트림으로부터 문서를 생성하고자 한다면 SAXBuilder를 이용해야 한다. SAXBuilder는 SAX 파서를 이용해 스트림을 읽은 뒤 SAX 파서 콜백에 따라 문서를 생성한다. 이 설계의 좋은 점은 SAX 파서의 속도가 빨라질수록 SAXBuilder도 빨라진다는 것이다. 그밖에 주요 입력 클래스는 DOMBuilder이다. DOMBuilder는 DOM 트리를 통해 문서를 생성한다. 이 클래스는 이미 존재하는 DOM 트리를 JDOM 버전으로 대신 사용하고자 할 경우 편리하다.
이러한 빌더의 잠재성에는 아무런 제한이 없다. 예를 들어, Xerces에는 SAX보다 더 낮은 수준에서 운용되는 XNI(Xerces Native Interface)가 있으므로 SAX를 통해 노출되지 않는 일부 파서 정보를 다루기 위해서 XNIBuilder를 사용하는 것이 적합할 수도 있다. JDOM 프로젝트를 지원해온 한 가지 대중적인 빌더는 ResultSetBuilder이다. 이 빌더는 JDBC ResultSet을 통해 SQL 결과를 다양한 구성의 요소(element)와 속성(attribute)을 가지는 XML 문서를 표현한다.
org.jdom.output 패키지에는 XML 문서를 출력하는 클래스가 포함돼 있다. 가장 중요한 클래스는 XMLOutputter이다. XMLOutputter는 파일, 스트림, 소켓으로 출력할 수 있도록 문서를 바이트 스트림으로 변환한다. XMLOutputter는 원시 출력, 가공 출력, 압축 출력 등을 지원하는 다수의 특별 구성 옵션을 가지고 있다. 이 클래스는 상당히 복잡하다. DOM Level 2에 아직도 이런 기능이 없는 것은 바로 이런 이유 때문일 것이다.
그 밖에 문서의 컨텐트를 기반으로 SAX 이벤트를 생성하는 SAXOutputter가 있다. 이 클래스는 모호해 보이기는 하지만 XSLT 변환시 매우 유용한데, 이는 문서 데이타를 엔진으로 전송하는 데 있어 SAX 이벤트가 바이트 스트림보다 훨씬 효율적인 방식이기 때문이다. 또한 문서를 DOM 트리 형식으로 표현하는 DOMOutputter도 있다. 그 밖에 수십 라인의 코드만으로 문서를 JTree로 보여주는 JTreeOutputter도 있는데, JTreeOutputter를 ResultSetBuilder와 함께 사용할 경우 코드 몇 라인만 추가하는 것만으로도 SQL 질의 결과를 트리 뷰로 나타낼 수 있다.
DOM과는 달리, JDOM에서는 해당 문서가 빌더에 구속되지 않는다는 점에 주목해야 한다. 따라서 데이타를 담는 클래스와 데이타를 구조화하는 다양한 클래스, 이 데이타를 사용하는 그 밖의 여러 클래스가 포함된 세련된 모델이 생성된다. 원하는 만큼 자유롭게 혼합해 사용할 수 있다.
org.jdom.transform 및 org.jdom.XPath 패키지에는 기본 XSLT 변환과 XPath 조회를 지원하는 클래스가 포함돼 있다.
마지막으로, org.jdom.adapters 패키지는 DOM 상호작용의 라이브러리를 지원하는 클래스를 포함하고 있는데, 이 패키지의 클래스를 호출할 필요가 전혀 없다. 이들 클래스가 존재하는 이유는 각 DOM의 구현 방식이 각각의 부트 스트래핑 작업 방식별로 서로 다른 함수 이름을 사용하기 때문이며, 이에 따라서 각 어댑터 클래스가 표준 콜을 파서 전용 콜로 번역한다. JAXP(Java API for XML Processing)는 어댑터 클래스가 과도하게 사용될 때의 문제점에 대한 대안으로서, 실제로 이들 클래스에 대한 요구를 감소시키는 역할을 한다. 그러나 모든 파서가 JAXP를 지원하는 것은 아니고, 또한 라이선스 문제 때문에 어디나 JAXP가 설치돼 있는 것도 아니기 때문에, 이러한 클래스들에 대한 필요성은 여전히 남아 있다.
문서의 생성
문서는 org.jdom.Documentclass에 의해 표현된다. 다음은 완전히 새로운 문서를 생성하는 경우이다.
// This builds: <root/>
Document doc = new Document(new Element("root"));
또한 파일이나 스트림, 시스템 ID, URL 등을 통해 문서를 생성할 수도 있다.
// This builds a document of whatever's in the given resource
SAXBuilder builder = new SAXBuilder();
Document doc = builder.build(url);
소수의 콜을 조합함으로써 간단한 JDOM 문서를 생성할 수도 있다.
// This builds: <root>This is the root</root>
Document doc = new Document();
Element e = new Element("root");
e.setText("This is the root");
doc.addContent(e);
파워유저라면 다양한 방법을 연속적으로 호출하는 'method chaining'선호할 것이다. 이 방식을 통해 여러 개의 메소드를 한 번에 호출할 수 있다. 다음은 method chaining의 예이다.
Document doc = new Document(
new Element("root").setText("This is the root"));
다음은 JAXP/DOM를 이용해 동일한 문서를 생성하는 예이다.
// JAXP/DOM
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.newDocument();
Element root = doc.createElement("root");
Text text = doc.createText("This is the root");
root.appendChild(text);
doc.appendChild(root);
SAXBuilder 이용하기
앞서 설명했듯이 SAXBuilder는 모든 바이트 근간 자원으로부터 문서를 생성하는 간단한 메커니즘을 제공한다. 입력 변수가 없는 기본 SAXBuilder() 생성자는 내부적으로 JAXP를 이용하여 SAX 파서를 선택한다. 파서를 변경하고자 할 때는 javax.xml.parsers.SAXParserFactory 시스템 속성을 파서가 제공하는 SAXParser Factory를 가리키도록 설정하면 된다. Oracle9i Release2 XML 파서의 경우 다음과 같이 실행하면 된다.
java -Djavax.xml.parsers.SAXParserFactory=
oracle.xml.jaxp.JXSAXParserFactory YourApp
Xerces 파서의 경우 다음과 같이 실행한다.
java -Djavax.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp
.SAXParserFactoryImpl YourApp
만약 JAXP가 설치돼 있지 않다면 SAXBuilder는 Apache Xerces를 기본값으로 이용한다. SAXBuilder 인스턴스가 생성된 뒤에는 다음과 같은 몇 가지 속성을 빌더에 설정할 수 있다.
setValidation(boolean validate)
WebLOCATOR
개방 소스 JDOM 라이브러리: jdom.org
Java Servlet Programming (second edition), (제이슨 헌터(Jason Hunter) 저) (2001년 O'Reilly & Associates 출간): www.oreilly.com |
이 메소드는 문서 생성 중 DTD(Document Type Definition)에 대해 검증할 것인지 여부를 파서에 알려준다. 기본으로 설정된 값은 false이다. 사용된 DTD는 문서의 DocType 내에서 참조된 것이다. 다른 DTD에 대해 검증하는 것은 아직 불가능한데, 이 기능을 지원하는 파서가 아직 없기 때문이다.
setIgnoringElementContentWhitespace(boolean ignoring)
위 메소드는 요소 컨텐트에서 ‘무시할 수 있는 여백(ignorable whitespace)’을 무시할 것인지 여부를 파서에 알려준다. XML 1.0 사양에 의하면, 요소 컨텐트의 여백은 파서에 의해 보존돼야 하지만 DTD에 대해 검증할 경우 문서의 특정 부분이 여백을 지원하지 않는다는 사실을 파서가 인식할 수 있기 때문에 이 영역의 여백은 ‘무시할 수’ 있다. 기본값으로는 해제 상태이다. 문서를 입력 때와 동일한 컨텐트를 출력하고자 할 때가 아니라면 ‘무시 가능’으로 사용하는 것이 일반적으로 성능상 바람직하다. 단, 이 플래그는 DTD 검증이 수행될 때만 유효하며 이때는 이미 검증 과정을 통한 성능 저하가 발생한 것이기 때문에 결국 이 메소드는 검증이 이미 이용되고 있을 경우에만 유용하다는 점을 유의해야 한다.
setFeature(String name, String value)
위 메소드는 기본 SAX 파서상에 기능을 설정하는 방법이다. 이 방법은 원시적인 호출 방식이기 때문에 이 방법을 이용할 때는 매우 신중해야 한다. 왜냐하면 특정 기능(예 : 네임스페이스 변경)을 잘못 설정할 경우 JDOM 작업이 중단될 수도 있기 때문이다. 게다가 파서 전용 기능에 의존할 경우 이식성을 제한할 위험이 있다. 이 콜은 스키마 검증을 선택할 때 가장 유용하다.
setProperty(String name, Object value)
위 메소드는 기본 SAX 파서상에 속성을 설정하는 방법이다. 이 방법 역시 원시 호출 방식으로, 위험한 동시에 특히 스키마 검증시 파워유저에게 유용한 방법이다. 다음 코드는 이 방법들을 조합해 검증 기능을 선택하고 여백 무시 가능 기능으로 설정한 뒤 JAXP 선택 파서를 이용해 로컬 파일을 읽게 된다.
SAXBuilder builder = new SAXBuilder();
builder.setValidation(true);
builder.setIgnoringElementContentWhitespace(true);
Document doc = builder.build(new File("/tmp/foo.xml"));
XMLOutputter를 이용한 문서 출력
문서는 다양한 포맷으로 출력될 수 있지만 가장 흔한 포맷은 바이트 스트림이다. JDOM에서는 XMLOutputter 클래스가 이 기능을 제공한다. 이 클래스의 기본 생성자는 문서에 저장된 원문 그대로 문서를 출력하려 한다. 아래 코드는 원문 그대로 문서의 내용을 파일에 출력하는 코드이다.
// Raw output
XMLOutputter outp = new XMLOutputter();
outp.output(doc, fileStream);
여백에 신경 쓰지 않아도 된다면 텍스트 트리밍을 선택해 약간의 공간을 절약할 수 있다.
// Compressed output
outp.setTextTrim(true);
outp.output(doc, socketStream);
사람 눈에 맞춰 문서의 인쇄 상태를 보기 좋게 만들려면 들여쓰기와 줄 바꿔쓰기를 추가하면 된다.
outp.setTextTrim(true);
outp.setIndent(" ");
outp.setNewlines(true);
outp.output(doc, System.out);
이미 여백을 통해 포맷된 문서에 위의 가공 기능을 다시 적용할 경우 트리밍을 선택해야 한다. 그렇지 않으면 이미 포맷된 상태에서 또다른 포매팅을 가하는 것이 돼 최종 출력 상태가 보기 흉하게 된다.
요소 트리의 네비게이션
JDOM은 요소 트리(element tree)의 네비게이션을 간편하게 해준다. 루트 요소를 호출하려면 다음 코드를 이용한다.
Element root = doc.getRootElement();
모든 자식 요소 리스트를 불러오는 방법은 다음과 같다.
List allChildren = root.getChildren();
주어진 이름의 요소만을 호출하려면,
List namedChildren = root.getChildren("name");
주어진 이름의 요소 중 첫 번째 요소만을 호출하려면 다음을 이용한다.
Element child = root.getChild("name");
getChildren() 콜을 통해 반환된 리스트는 모든 Java 프로그래머가 알고 있는 리스트 인터페이스의 구현인 java.util.List이다. 이 리스트에서 특기할 만한 것은 이것이 라이브 리스트라는 점이다. 리스트에 가해진 모든 변경사항은 원본 문서 객체에도 반영된다.
// Remove the fourth child
allChildren.remove(3);
// Remove children named "jack"
allChildren.removeAll(root.getChildren("jack"));
// Add a new child, at the tail or at the head
allChildren.add(new Element("jane"));
allChildren.add(0, new Element("jill"));
이러한 리스트를 통한 대치 방법을 이용하면, 수많은 별도의 방법들을 과도하게 사용하지 않고도 요소를 다양하게 가공할 수 있다. 그러나, 편의상 주로 이용하는 작업인, 마지막에 요소를 추가하거나 이름이 있는 요소들을 삭제하는 경우 요소 자체에 이미 동일한 메소드가 포함돼 있기 때문에 이 작업을 실행할 때는 리스트를 우선 호출할 필요가 없다.
root.removeChildren("jill");
root.addContent(new Element("jenny"));
JDOM의 또 다른 장점은 한 문서 내에서 혹은 여러 문서 사이에서 요소들을 이동하는 작업이 간편하다는 것이다. 이 때 몇 개의 문서간에 이동하든 관계없이 동일한 코드를 사용할 수 있다.
Element movable = new Element("movable");
parent1.addContent(movable); // place
parent1.removeContent(movable); // remove
parent2.addContent(movable); // add
DOM의 경우 요소의 이동이 JDOM에서만큼 쉽지 않은데, 이는 DOM에서는 요소들이 그들을 생성한 객체에 강하게 묶여 있기 때문이다. 따라서 문서간 이동시에는 DOM 요소가 직접 '임포트' 되어야 한다.
JDOM에서 한 가지 유념할 사항은, 요소를 다른 데 추가하기 전에 제거해야 한다는 점이다. 이렇게 해야 트리 내에서 순환이 발생하는 것을 막을 수 있다. detach() 메소드를 이용하면 분리/추가 작업을 라인 하나로 처리할 수 있다.
parent3.addContent(movable.detach());
요소를 다른 부모에 추가하기 전에 먼저 분리하지 않았을 경우, 해당 라이브러리는 Exception을 떨어트릴 것이다(정확하고 도움이 되는 오류 메시지와 함께). 또한 라이브러리는 요소에 스페이스와 같은 부적절한 문자가 포함되지 않도록 요소의 이름과 컨텐트를 확인한다. 또한 단일 루트 요소의 포함 여부, 일관적인 네임스페이스 선언 여부 및 주석과 CDATA 섹션에 금지된 문자열이 없는지 등 기타 여러 규칙도 검증한다. 이를 통해 가능한 한 프로세스 초기 단계에서 문서가 'well-formed' 인지 확인하는 과정이 이루어지게 되는 것이다.
요소 속성의 처리
요소 속성의 예를 들면 다음과 같다.
<table width="100%" border="0"> ... </table>
요소 참조를 통해, 어떤 이름의 속성 값이든 요소에 요청할 수 있다.
String val = table.getAttributeValue("width");
또한 타입 변환과 같은 특별 가공 작업을 위해 속성을 객체로 불러올 수도 있다.
Attribute border = table.getAttribute("border");
int size = border.getIntValue();
속성을 설정하거나 변경하려면 setAttribute()를 사용한다.
table.setAttribute("vspace", "0");
속성을 삭제하려면 removeAttribute()를 사용한다.
table.removeAttribute("vspace");
텍스트 컨텐트를 가진 요소의 예를 들면 다음과 같다.
<description>
A cool demo
</description>
JDOM에서는 호출을 통해 텍스트를 직접 이용할 수 있다.
String desc = description.getText();
한 가지 유의할 점은, XML 1.0 사양에서는 여백의 보존이 요구되기 때문에 이 경우 '\n A cool demo\n'반환된다는 것이다. 그러나 실제 환경에서는 여백의 포매팅에 대해 크게 유념할 필요가 없으므로 가장자리의 여백을 무시하고 텍스트를 불러오는 편리한 방법이 있다.
String betterDesc = description.getTextTrim();
여백을 아예 없애고 싶다면 스페이스를 이용해 내부의 여백을 표준화하는 getTextNormalize() 메소드를 이용하면 된다. 이 메소드는 다음과 같은 텍스트 컨텐트에 이용할 때 편리한다.
<description>
Sometimes you have text content with formatting
space within the string.
</description>
텍스트 컨텐트를 변경하고자 할 때는 setText() 메소드를 이용한다.
description.setText("A new description");
텍스트에 포함된 특수 문자는 모두 문자로서 올바르게 해석되어 출력시에 적절한 의미를 유지하게 된다. 다음 콜을 예로 들어보자.
element.setText("<xml/> content");
내부 저장 영역은 이 문자열을 그대로 문자로 저장할 것이다. 이 컨텐트에 대한 함축적 파싱은 이루어지지 않는다. 출력시에는 다음과 같이 표현된다.
<xml/> content<elt>
이는 이전 setText() 콜의 의미론적 내용을 보존하기 위한 것이다. 따라서, 요소 내에 XML 컨텐트를 포함하고자 한다면 적절한 JDOM 자식 요소 객체를 추가해야 할 것이다.
JDOM에서는 CDATA 섹션을 처리할 수도 있다. CDATA 섹션은 파싱되어서는 안 될 텍스트 블록을 지시한다. 원래 CDATA 섹션은 <와 > 같은 에스케이프 문자열을 과도하게 사용하지 않고도 HTML이나 XML을 손쉽게 포함시킬 수 있게 해주는 문법적인 용어이다. 그러나 JDOM에서는 이를 객체화하여 사용한다. CDATA 섹션을 생성하려면 이 문자열을 CDATA 객체로 래핑하면 된다.
element.addContent(new CDATA("<xml/> content"));
JDOM의 장점은 getText() 콜이 문자열을 CDATA 섹션으로 나타낼 것인지 일일이 물어보지 않고 문자열을 반환한다는 점이다.
혼합 컨텐트의 처리
어떤 요소에는 여백, 주석, 텍스트, 자식 요소 등 수많은 항목들이 포함되어 있다.
<table>
<!-- Some comment -->
Some text
<tr>Some child element</tr>
</table>
어떤 요소에 텍스트와 자식 요소가 모두 들어 있을 경우 ‘혼합 컨텐트’를 포함하고 있다고 한다. 혼합 컨텐트를 처리하는 것은 어려울 수도 있지만, JDOM을 이용하면 쉽게 처리할 수 있다. 텍스트 컨텐트를 불러오고 자식 요소를 네비게이션하는 기본 이용 사례는 간단하게 처리할 수 있다.
String text = table.getTextTrim(); // "Some text"
Element tr = table.getChild("tr"); // A straight reference
주석, 여백 블록, 프로세싱 명령어, 엔티티 참조 등이 필요한 고급 응용의 경우, 혼합 컨텐트를 리스트로서 직접 불러올 수 있다.
List mixedCo = table.getContent();
Iterator itr = mixedCo.iterator();
while (itr.hasNext()) {
Object o = i.next();
if (o instanceof Comment) {
...
}
// Types include Comment, Element, CDATA, DocType,
// ProcessingInstruction, EntityRef, and Text
}
자식 요소 리스트와 마찬가지로 혼합 컨텐트 리스트를 변경할 경우 원래 문서에도 영향을 미치게 된다.
// Remove the Comment. It's "1" because "0" is a whitespace block.
mixedCo.remove(1);
자세히 살펴보면, 여기에 Text 클래스가 포함돼 있다는 사실을 알 수 있다. JDOM은 내부적으로 Text 클래스를 이용해 문자열 컨텐트를 저장하는데, 이는 이 문자열이 부모를 갖도록 하고 XPath 액세스를 보다 쉽게 지원하도록 하기 위한 것이다. 원시 컨텐트 리스트에 액세스할 때 텍스트만을 불러오거나 설정할 경우라면 이 클래스에 대해 염려할 필요가 없다.
DocType, ProcessingInstruction, EntityRef 클래스에 대해 보다 자세한 정보는 jdom.org의 API 설명서를 참조하기 바란다.
원문 출처 : http://www.oracle.com/global/kr/magazine/webcolumns/2002/o52jdom.html