java_codes

전에 개발자로 참여했던 웹 프로젝트가 있었는데, UI 개발할 때 상용 소프트웨어를 사용하고 있었다. 개발자는 Drag & Drop으로 Component를 위치시킨 후 속성들을 에디터에서 수정할 수 있고, 이러한 정보들이 내부적으로 XML Document 형태로 저장되는 방식이다. 그러던 어느날 일괄적으로 어느 Component의 기본 속성들을 바꿀 일이 생겼다. 이미 너무 많은 파일과 Component들을 만들어둔 상태에서 고민에 빠졌다. 노가다로 할 것인가? 아니면 다른 방법을 시도할 것인가? 나의 선택은 후자였다.

Document 읽기

XML을 읽기 위헤서는 DocumentBuilderFactoryDocumentBuilder를 만들고 이를 #parse하면 된다.

private static final DocumentBuilderFactory documentBuilderFactory;

static{
    documentBuilderFactory = DocumentBuilderFactory.newInstance();
    documentBuilderFactory.setNamespaceAware(true); // true로 해야지만 namespace를 인식할 수 있다
}

public Document parseXML(String pathname) throws Exception {
    DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
    // #newDocumentBuilder로 매번 인스턴스를 생성한다
    
    return documentBuilder.parse(new File(pathname));
}

Static Attributes : DocumentBuilderFactory는 Thread-Safe하다. (test : junit). 하지만 documentBuilder는 Thread-Unsafe 하다. (출처 : stackoverflow) 둘 다 static으로 하고 싶었지만, documentBuilder는 인스턴스로 해야만 한다.

XPath로 원하는 element 찾기

XPath Instance

Document를 불러들였으면 이를 XPath로 읽어들이면 된다.

XPath xPath = XPathFactory.newInstance().newXPath();

만약 XML 파일에 namespace가 존재할 때, prefix에 bound된 namespaceURI를 찾기 위한 메서드를 @Override 하면 된다.

XPath xPath = XPathFactory.newInstance().newXPath();
xPath.setNamespaceContext(new NamespaceContext() {
    @Override
    public String getNamespaceURI(String prefix) {
        if("ns".euqlas(prefix)) {
            return "http://www.w3.org/2000/xmlns/";
        } else {
            return "";
        }
    }
    
    @Override
    public String getPrefix(String namespaceURI) {
        return null;
    }
    
    @Override
    public Iterator<String> getPrefixes(String namespaceURI) {
        return null;
    }
});

XPath로 Document를 포함한 Node의 객체를 탐색할 수 있다.

String expression = "";
Document document;
NodeList nodeList = null;
try {
    nodeList = (NodeList) xPath.evaluate(expression, document, XPathConstants.NODESET);
} catch (XPathExpressionException e) {
    e.printStackTrace();
}

XPathConstants.NODESETNode 타입의 항목만 탐색하게 되며, 조회된 결과가 없을 경우 getLength()가 0인 NodeList가 반환된다.

expression 예제

태그명으로 Node를 찾는 방법은 다음과 같다.

expression output
/html root인 <html>
//td 모든 <td>
//tr/td parent가 <tr><td>
//table/* parent가 <table>인 모든 Node
//table//td ancestor 중에 <table>이 있는 <td>

특정 Node의 속성을 활용하여 필터링을 할 수도 있다.

expression description
//td[@style] style 속성이 존재 (공백포함)
//td[@colspan=2] colspan 속성이 2
//td[@colspan>=3] colspan 속성이 3 이상
//table[@id='tb_main']/td (ancestor도 필터링을 할 수 있다)

또한 다양한 함수를 지원한다.

expression description
//td[position() = 1] 첫번째 child
//td[contains(@class, 'text-right')] text-right를 포함하는 class

element 수정하기

expression으로 특정 Node 또는 NodeSet을 구한 뒤 객체를 수정한다.

for(int i=0; i<nodeList.getLength(); i++){
    Element e = (Element) nodeList.item(i);
    
    // nodeValue를 수정할 때
    e.setNodeValue("new value");
    
    // attribute를 수정할 때
    e.setAttribute("attribute name", "new value");
}

Document 저장하기

읽을 때와 비슷하게 저장할 때는TransformerFactory를 활용한다.

private static final TransformerFactory transformerFactory = TransformerFactory.newInstance();

public Document saveXML(File file, Document document) throws Exception {
    DOMSource domSource = new DOMSource(document);
    StreamResult streamResult = new StreamResult(file);

    Transformer transformer = transformerFactory.newTransformer();
    transformer.transform(domSource, streamResult);
}

이렇게 수정한 내용을 파일에 저장할 수 있다. 노가다로 하려던 수정작업을 XPath를 활용하여 쉽게 할 수 있게 되었다. 다만 이렇게 파일로 저장하게 되면 새로 XML 파일을 저장하는 과정에서 공백이나 줄바꿈 등이 바뀌어버렸다. commit할 때 whitespace에 의한 수정인지 아닌지를 확인해봐야 하는 번거로움이 있었지만, 그래도 수고로움은 아낄 수 있었다.

나는 XPath로 문서를 다룰 때 파일 읽기/쓰기를 담당하는 FileIOHandler와 XPath를 담당하는 XmlFile로 객체를 나누었다. 테스트 코드는 GitHub 에서 확인할 수 있다.