[펌] XML DATA

XML의 CData

오늘 오전시간을 XML CDATA Section 오류 디버깅을 하면서 보냈다. MySQL DB에 있는 테이블을 XML로 덤프하여 색인용 원시 텍스트파일을 만드는 작업을 하던 도중에 복병(?)을 만났기 때문이다. Python으로 MySQL 테이블을 읽어 print 문을 사용하여 XML 파일을 생성하고 난 후에 Java SAXParser로 읽는 작업이었는데 "org.xml.sax.SAXParseException: An invalid XML character (Unicode: 0x{2}) was found in the CDATA section" 오류가 났다. 4백만건이 넘는 대용량 테이블이었기 때문에 Java SAXParser에서 이 오류를 한번 볼 때 마다 Python code를 점검하여 다시 덤프받느라 고생했다.

색인용 원시 텍스트파일 포멧은 주로 텍스트 파일을 사용한다. 색인 원문이 저장된 장비와 검색장비 간에 플랫폼이 다를때 발생하는 endian 문제를 피하기 위해서다. XML은 이런 장점에 더해 문서 구조를 더 잘 표현할 수 있는 장점이 있어 일부 검색엔진의 경우 색인용 원시 텍스트파일로 UTF-8 인코딩된 XML을 사용하기도 한다. 검색 콜렉션을 담고 있는 대용량 DB 테이블에서 색인용 XML을 덤프받을때 덤프 속도 문제로 DOMWriter와 같은 validation을 하는 XML Generator를 사용하지 않고 print를 사용해 validation 없이 종종 직접 생성한다. 또 골치아프고 속도를 느리게 만드는 char escaping을 하지 않기 위해 CDATA 섹션으로 필드를 감싼 형태로 덤프하곤한다.

현업에서 이런 방식으로 XML을 많이 사용하기 때문에 위 오류에 대한 대처 방안을 구글 검색을 통해 쉽게 찾을 줄 알았다. 한시간 가량이나 구글링을 해서야 원인을 찾았다. SAXParseException은 validation 없이 CDATA 섹션을 만들기 때문이었다. 데이터베이스에 레코드를 넣을 때 print문을 사용해 직접 덤프하도록 준비해서 넣지 않기 때문에 제어문자와 같은 unicode도 함께 저장되어 덤프 후 XML을 다시 파싱할 때 이런 종류의 문자를 파싱할 때SAXParse 오류가 발생한다. 파싱 오류를 해결하려면 CDATA 섹션안에 들어가면 안되는 unicode range를 print문으로 XML을 덤프할 때 제거해야 한다. 이 unicode range는 XML 스펙 1.0에 다음과 같이 명기되어 있다.

Character Range
[2]  Char   ::=  #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]/* any Unicode character, excluding the surrogate blocks, FFFE, and FFFF. */


CDATA Sections
[18]  CDSect   ::=  CDStart CData CDEnd
[19]  CDStart   ::=  '<![CDATA['
[20]  CData   ::=  (Char* - (Char* ']]>' Char*))
[21]  CDEnd   ::=  ']]>'


위를 보면 CDATA 섹션안의 문자열에는 2가지 들어가지 말아야 하는 조건이 있다.

첫째. CDATA 종료열인 ']]>' 문자열이 들어가면 안된다.
둘째. 유니코드 문자만 포함되어야 한다. 특히, 제어문자들은 포함되지 말아야 한다.

위 조건을 만족하도록 print문으로 XML 파일을 만들면 SAXParseException이 나타나지 않는다. 비록 문자들을 모두 scan하면서 위 조건에 맞는지 조사해야 하기 때문에 수백만 건이 포함된 테이블을 XML로 덤프할 때는 조사 시간이 누적되어 월씬 덤프하는데 오래 걸리게 되지만 SAXException을 구경하는 것 보다는 좋다.

대용량 문서 때문에 validation을 하지 않는 방식으로 XML을 생성해야만 하는 경우에 가장 좋은 방법은 데이터베이스에 넣을 때 아에 위 문자들이 들어가지 않도록 방지하거나 제거하는 것이다.

@윤종완

  /**
     * This method ensures that the output String has only
     * valid XML unicode characters as specified by the
     * XML 1.0 standard. For reference, please see
     * <a href="http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char">the
     * standard</a>. This method will return an empty
     * String if the input is null or empty.
     *
     * @param in The String whose non-valid characters we want to remove.
     * @return The in String, stripped of non-valid characters.
     */
    public String stripNonValidXMLCharacters(String in) {
        StringBuffer out = new StringBuffer(); // Used to hold the output.
        char current; // Used to reference the current character.

        if (in == null || ("".equals(in))) return ""; // vacancy test.
        for (int i = 0; i < in.length(); i++) {
            current = in.charAt(i); // NOTE: No IndexOutOfBoundsException caught here; it should not happen.
            if ((current == 0x9) ||
                (current == 0xA) ||
                (current == 0xD) ||
                ((current >= 0x20) && (current <= 0xD7FF)) ||
                ((current >= 0xE000) && (current <= 0xFFFD)) ||
                ((current >= 0x10000) && (current <= 0x10FFFF)))
                out.append(current);
        }
        return out.toString();
    }    

주1) 마크 맥라렌의 블로그에 둘째 조건을 체크해 오류 문자를 제거하는 Java 코드를 소개하고 있다. 관련된 trackback도 읽으면 도움이 된다.

주2) 위 방법은 XML 1.0 스펙을 준수하는 XML Parser에 적용된다. XML 1.1 스펙을 보면 Char Range가 좀 다르다.
XML 1.1을 준수하는 Xerces와 같은 경우에는 다른 현명한 방법을 제공하고 있을지도 모르겠다