GnuSmalltalkUsersGuide:AdditionalChapter8 1
- XML로부터 DOM 빌드하기
XML로부터 DOM 빌드하기
당신이 필자와 같다면 일종의 XML 입력으로부터 문서 객체 모델(DOM) 트리를 빌드하는 일을 가장 먼저 시도할 것이다. String으로 된 XML이 있다고 가정할 때 아래의 코드는 XML Document를 빌드할 것이다:
XML XMLParser processDocumentString: theXMLString
beforeScanDo: [ :p | p validate: false].
위의 코드는 사용하기 쉬운 듯해 보이지만 당신이 알아야 하는 기능이 몇 가지 숨어 있다. 첫째, theXMLString 은 어떤 널(null) 바이트도 포함할 수 없다. 자신의 XML 가 어디서부터 오는지에 따라 NULL 바이트를 끝에 가질 수도 있다 (나와 마찬가지로). 많은 언어들이 문자열을 null로 끝이 나는 바이트의 배열(주로 인쇄 가능한)로 구현한다. 필자의 경우, 내 서버로 메시지를 전송하기 위해 미들웨어(middleware)를 이용해 C로 작성된 원격 클라이언트로부터 XML가 왔다. 미들웨어는 그것이 수신한 메시지에 관한 내용을 알 것이라고 가정하지 않기 때문에 널 바이트를 포함해 String으로서 수신된다. 이를 제거하기 위해 아래를 사용했다.
XML XMLParser processDocumentString: (aString copyWithout: 0 asCharacter)
beforeScanDo: [ :p | p validate: false].
처음에는 DTDs(문서형 정의)의 값에 대해 많이 알지 못했기 때문에 사용하지 않았다 (사용해야 하는 이유는 후에). 당신이 알아야 할 것은 XML이 두 가지(broken도 방식에 포함시킨다면 세 가지), 즉 well-formed(잘 구성된)와 valid(유효한) 로 나뉜다는 점이다.
잘 구성된 XML은 중복되는 태그 없이 하나의 최상위 수준(문서의 루트)과 같은 기본 규칙과 몇 가지 다른 제약을 따르는 XML이다. 그리고 유효한 XML은 XML이 잘 구성되었을 뿐만 아니라 어떤 요소들이 다른 요소들을 따르도록 허용되어야 하는지, 속성이 허용되어야 하는지, 그들의 값은 무엇인지, 기본값은 무엇인지에 대한 규칙 기반도 어느 정도 준수함을 의미한다.
정형성(well-formedness)을 피하는 방법은 없다. 대부분 XML 툴은 누락된 태그나 오픈 태그를 불평한다. 아무렇게나 할 수 없는 것은 아마도 XML가 어떻게 집합(assemble)되어야 하는지를 설명하는 DTD일 것이다. 어떤 이유로든 검증을 건너뛰어야 한다면 아래 선택자를 이용해야 한다:
beforeScanDo: [ :p | p validate: false].
이제 XML 문서가 있으니 그 내용으로 접근하길 원할 것이다 (그 이유가 아니라면 문서를 가진 목적도 없을 테니 말이다). 아래 (간략한) XML을 예로 살펴보자:
<porder porder_num="10351">
<porder_head>
<order_date>01/04/2000</order_date>
</porder_head>
<porder_line>
<part>widget</part>
<quantity>1.0000</quantity>
</porder_line>
<porder_line>
<part>doodad</part>
<quantity>2.0000</quantity>
</porder_line>
</porder>
가장 먼저 아마도 서로 다른 태그로 어떻게 접근하는지, 좀 더 구체적으로 말해 그러한 태그의 내용으로 어떻게 접근하는지가 궁금할 것이다. 첫째, 당신이 문서를 할당한 변수가 doc으로 명명되었다고 가정하고, 요소에 로드맵을 제공하는 방식을 통해 문서의 다른 조각을 받기 위한 스몰토크 코드를 보여줄 것이다. 진행하면서 다양한 요소에 대한 인스턴스 변수도 생성하고자 한다.
원하는 요소 | 그것을 얻기 위한 코드 |
porder element | doc root |
porder_head | doc root elementNamed: 'porder_head' |
order_date (String으로) | (porderHead elementNamed:'order_
date') characterData |
order_date (Date로) | (Date readFrom: (porderHead
elementNamed: 'order_date') characterData readStream) |
두 개의 porder_lines으로 된 컬렉션 | doc root elementsNamed: 'porder_line' |
porder의 속성으로 접근하는 방법은 다른 노드로의 접근과는 다르기 때문에 고의적으로 생략하였다. 아래를 이용하면 속성의 OrderedCollection을 얻을 수 있다:
attributes := doc root attributes.
하지만 정렬된 컬렉션은 사실 그다지 유용하지 않다. 어떤 단일 속성으로 접근하든 컬렉션에서 검색해야 한다:
porderNum := (attributes detect: [ :each | each key type = 'porder_num' ]) value.
하지만 이것이 재미있는 일만은 아닌데, 특히 얻어야 하는 내용이 많거나 속성이 존재하지 않은 가능성이 있을 때 그러하다. 이런 경우 detect: ifNone: 를 실행해야 하는데, 어쩜 그렇게 코드를 읽기 쉽게 만드는지! 이 대신 필자는 객체의 abstract에 메서드를 생성했다:
dictionaryForAttributes: aCollection
^Dictionary withAll: (aCollection
collect: [ :each | each key type -> each value ])
이제 점차적으로 속성을 얻는 데에 더 유용한 메서드를 가질 것이다:
attributes := self dictionaryForAttributes: doc root attributes.
porderNum := attributes at: 'porder_num'.
처음엔 코드가 늘어난 것처럼 보이며, 단일 속성이라면 사실상 늘어난 것이 맞다. 하지만 요소가 하나 이상의 속성을 포함한다면 그 결과는 꽤 괜찮다. 물론 여전히 dictionary 내에 속성이 없다는 상황을 처리해야 하긴 하지만 OrderedCollection 대신 Dictionary를 사용하면 약간 더 잘 읽히는 것으로 생각된다:
porderNum := attributes at: 'porder_num' ifAbsent: [].