|
|
데이터베이스에 들어 있지 않은 데이터는 거의 XML 데이터 형식으로 표준화되었다. XML은 구질구질하지만, 견고하며, XML을 다루는 API도 다양하다. 파싱을 하든, 데이터 바인딩을 하든, 또는 변형(transforming)을 하든, XML을 사용하지 않는 애플리케이션은 제한적이고, 조금은 시대에 뒤떨어져 보인다.
겉보기에는 아무 연관 없는 두 가지 사실, 즉 데이터를 SQL 데이터베이스에 저장하고 데이터베이스 밖의 모든 데이터는 XML로 관리하려는 성향은 독특한 문제를 만들었다. SQL 데이터베이스는 쿼리(query)가 쉽지만, XML 문서는 그렇지 못하다. 사용자들은 데이터를 쉽게 검색하기를 원한다. 데이터베이스 안에 있는 데이터는 검색이 잘 되지만, XML 안에 있는 데이터는 검색하기 어렵다. 분명한 점은, XML 형식의 데이터를 단지 검색을 쉽게 하기 위해 데이터베이스에 저장하는 것은 잘못된 접근 방식이라는 것이다. 이 때문에 XQuery가 등장했고, XQJ(XQuery API for Java)가 만들어졌다.
|
|
간단히 말하면 XQuery는 XML 문서를 검색하는 데 쓰는 언어다. 마치 SQL이 SELECT와 FROM에 특별한 의미를 부여하듯이 — 어떤 특별한 문맥에서 — XQuery는 슬래시(/
), 골뱅이(@
)와 몇 가지 키워드에 의미를 정의한다.
XQuery는 세 개의 핵심 요소로 이루어져 있다.
XQuery를 제대로 익히려면 위 세 가지 구성요소를 모두 알아야 한다. 자바 프로그래머라면, XPath를 배우고, 추가 XPath 구문을 배우고, XQuery 표현식을 평가하기 위한 자바 기반 API를 사용해야 한다.
다행스럽게도 XPath와 XQuery구문은 상당히 직관적이다. 유닉스(UNIX?) 셸, 맥 OS X 터미널, DOS 창에서 디렉터리 구조를 사용해 본 적이 있다면 쉽게 이해될 것이다. 거기에 <
, >
, =
같은 기본적인 연산자를 알고 있다면 XPath 전문가가 되는 길에서 절반은 온 셈이다.
|
XQuery는 XPath라는 또 다른 XML 스펙에 전적으로 의존한다. XPath는 XML 문서의 일부를 나타내는 경로(path)를 생성하는 방법을 정의한다. 예를 들어, /play/act/scene
은 play
루트 요소(root element) 안에 있는 act
요소를 찾고, 그 안에 속해 있는 모든 scene
요소를 가리킨다.
기본적인 XPath는 요소 이름과 슬래시(/)를 사용한다. 그리고 기본 값으로, XPath는 XML 문서의 현재 위치에서 시작한다. 예를 들면, DOM을 사용하고 있고, speech
요소로 가서, speaker
경로를 찾으면 현재 위치인 speech
요소 안에 있는 모든 speaker
요소를 가리킨다. 즉, XPath는 문서 안에서 현재 위치로부터 상대적으로 평가한다.
문서의 루트로 이동하려면, 경로 앞에 슬래시(/)를 붙이면 된다. 따라서 /play
는 현재 어느 위치에 있든 play
라는 루트 요소를 찾는다. ../
를 붙이면 현재 요소의 부모 요소를 찾는다. 이러한 것들은 디렉터리 구조와 비슷하다. 예를 들어, ../../personae/title
은 현재 요소에서 두 단계 위로 올라간 후 personae
요소를 찾고, 그 안에 있는 title
요소를 찾는다.
XPath는 요소 외에도 많은 것을 검색할 수 있다. 예를 들어, /cds/cd@title
은 cds라는 이름의 루트 요소 안에 있는 cd
요소에서 title이라는 이름의 모든 속성(attribute)을 반환한다.
@
구문은 속성의 값이 아닌 속성 자체를 반환한다는 점을 주의해야 한다. @isbn
은 isbn 이라는 속성의 값이 아닌, isbn 이라는 이름의 모든 속성을 검색한다. XPath에서 속성이라는 용어는 속성의 이름과 값을 의미한다(더 자세한 내용은 노드 선택하기를 참조하라).
|
요소와 속성을 검색하는 것처럼, 요소 안에 있는 텍스트도 검색할 수 있다. 예를 들어, /cds/cd/title
같이 XPath가 요소의 이름으로 끝나면 그 요소 안에 있는 텍스트를 포함하지 않는다. 요소 안에 있는 텍스트를 검색하려면, text()
구문을 사용하면 된다. CD들의 모든 title 을 검색하고 싶다면 /cds/cd/title/text()
를 사용하면 된다. 이러한 경로는 요소를 반환하는 것이 아니라, 요소 안에 있는 텍스트를 반환한다.
효과적으로 XPath를 사용하려면, XPath의 평가 결과가 항상 노드 집합(node set)이라는 점을 명심해야 한다. 그 집합은 비어있을 수도 있고, 하나 이상의 노드들을 포함할 수도 있다. 어쨌든 XPath의 결과는 항상 집합이다. 경로는 요소나 속성이나 텍스트를 반환한다는 일반적인 생각을 포함하고 있지만 그게 전부는 아니다.
DOM을 사용한다면 노드에 대해 생각해본 적이 있을 것이다. DOM에서는, XML 문서 안에 있는 모든 것, 즉 요소, 속성, 텍스트는 노드다. 요소는 요소 노드(element node), 속성은 속성 노드(attribute node)다. 심지어 요소 안에 있는 텍스트도 텍스트 노드(text node)다. 따라서 ../../personae/title
은 title
요소를 검색하는 경로지만, 사실 노드 집합을 반환한다. 그 집합은 비어있거나(검색 조건에 맞은 요소가 없거나) 여러 개의 노드를 포함할 수 있다. 이 경우에는 반환된 노드 집합에 포함된 모든 노드가 "title"이라는 이름의 요소일 것이다.
경로가 점점 복잡해지면서, 잠재적으로 더 넓은 집합 — 속성과 요소, 또는 텍스트와 요소를 동시에 포함하는 — 을 선택할 수 있겠지만, 결론은 노드 집합을 선택하는 것이다. XQuery를 잘 사용하려면 이 점을 명심해야 한다. XPath를 사용하면 노드 집합을 선택할 수 있다. 그리고 XQuery와 함께 사용하면, 검색 조건으로 그러한 노드들의 일부분을 선택하거나 여러 노드 집합을 합쳐 검색 조건에 적용할 수 있다. 노드 집합은 여러 가지 형태(요소, 텍스트, 속성)라는 것을 염두에 둔다면, 경로를 좀 더 나은 방법으로 사용할 수 있고, 그 경로들은 원하는 것을 그대로 반환할 것이다.
|
지금까지 노드(요소 또는 속성)와 부모 노드(텍스트나 주어진 노드의 모든 자식 노드를 선택할 경우)의 이름으로 노드 집합을 선택하는 방법을 알아 보았다. 이것만으로도 XPath는 꽤 강력하지만, XPath는 검색을 위한 몇 가지 술어(predicate)를 더 제공한다.
|
술어는 노드 집합에 적용될 수 있는 표현식(expression)이며, 대괄호([
, ]
)로 표기한다. 술어는 그 술어의 왼쪽에 있는 경로가 가리키는 노드 집합에 적용된다. 예를 들어, /cds/cd
라는 경로는 루트 요소인 cds 안에 있는 모든 cd
요소를 선택한다. 그런데 첫 번째 CD를 원한다면 /cds/cd[1]
와 같이 술어를 사용하면 된다. 이 경로는 /cds/cd
경로가 선택한 노드들 중에서 첫 번째 노드를 반환한다.
술어는 그 술어의 바로 왼쪽에 있는 노드 집합에 적용된다는 것을 기억하라. 그렇다고 해서 술어가 오직 완전한 XPath의 끝 부분에만 사용할 수 있다는 뜻은 아니다. XPath가 경로들의 집합이라고 생각한다면, 각각의 경로는 각각의 노드 집합을 반환할 것이다. 예를 들어, /cds/cd/title
은 다음과 같이 세 개의 경로를 포함한다.
/cds
는 "cds"라는 이름의 루트 요소를 반환한다(하나의 요소 노드를 가진 노드 집합).
cd
(앞의 노드 집합에 상대적으로)는 앞의 노드 집합 안에 있는 모든 cd
요소를 반환한다.
title
(cd와 마찬가지로, 앞의 노드 집합에 상대적으로)은 앞의 노드 집합 안에 있는 모든 title
요소를 반환한다. 술어는 반드시 노드 집합에 적용되어야 하며, 어느 노드 집합에도 적용될 수 있다. 예를 들어, /cds[1]/cd[2]/title[1]
은 올바르게 사용된 경로인데, /cds
로 검색된 첫 번째 노드 집합을 선택하고, /cds[1]/cd
로 두 번째 노드 집합을 선택한다. 그리고 여기서 /cds[1]/cd[2]/title
로 선택된 노드들 중에서 첫 번째 노드를 선택한다.
주의: 이 경로의 각 부분은 특별한 의미가 없다. 예를 들어, /
로 투트 요소를 선택한 다음, [1]
술어를 적용하면 항상 집합 내의 첫 번째이자 유일한 노드를 반환한다. 술어가 요소를 반환하지 않는 유일한 경우는 경로에 지정한 루트 요소가 문서의 실제 루트 요소와 달라 노드 집합이 비어있을 경우뿐이다. 그러나 설명을 위해서건 기술적인 관점이건 XPath 자체만 사용해도 되고, 술어를 사용해도 된다.
숫자만으로 참조하도록 하는 API는 극히 드물지만, 그렇게 하면 항상 원하는 항목이 정확히 어디에 있는지 알 수 있다. XPath에는 그 외에도 많은 술어를 제공한다. 일단, last()
함수를 사용하면 얼마나 많은 항목이 있든 그 중에서 마지막 항목을 선택할 수 있다. 예를 들어, /cds/cd[last]
는 문서에서 마지막 cd
를 선택한다.
position()
함수를 사용하면 어떤 특정 위치보다 앞에 있거나 뒤에 있는 항목을 선택할 수 있다. position()
함수는 주어진 노드들 속에서 위치값을 반환한다. 예를 들어, 처음 다섯 개의 CD들을 검색하기 원한다면 /cds/cd[position()<6]
패스를 사용하면 된다. 이 패스는 position()
값이 6보다 작은 모든 노드를 선택한다.
마지막으로 — 지금 보고 있는 XPath에 대한 짧은 소개 글에서 — 노드의 자식 요소나 노드의 속성을 기준으로 노드를 선택할 수 있다. XPath에서는 순차적인 위치가 상위 경로의 노드들을 기준으로 하듯이, 노드들의 술어는 그 술어가 적용된 노드들을 기준으로 한다. 술어에 연산자 <
, >
를 덧붙여 사용하면 검색된 노드들 중에서 단순히 위치가 아닌 데이터를 기준으로 선택할 수 있다.
속성 이름이 "style"이고 값이 "folk"인 CD들을 검색해보자. 먼저 모든 CD를 검색하는 표현식을 사용하고, 그러고 나서 그 CD들의 style
속성을 "folk"로 비교한다. XPath로 표현한다면 /cds/cd[@style='folk']
가 될 것이다. 지금까지 설명한 것에 따르면 이것은 상당히 이해하기 쉽다. 먼저, /cds/cd
로 모든 cd 노드를 선택한다. 그리고 @style
술어로 각 노드의 속성 이름이 "style"인 것을 분류한다. 앞에서 말했듯이 이름 앞에 @
가 오는 것은 속성을 가리킨다. 그리고 속성은 이미 선택된 노드들(이 경우는 모든 cd
요소)에 상대적이라고 가정한다. 그러고 나서 이 속성 값들을 "folk"와 비교한다. 그리고 일치하는 속성을 가진 노드들이 반환된다. 그 외 다른 것들은 결과 값에서 빠진다.
같은 과정이 선택된 노드들 안에 있는 요소에도 적용된다. Listing 1과 같은 구조를 가진 문서가 있다고 가정하자.
<cds> <cd style="some-style"> <title>CD title</title> <track-listing> <track>Track title</track> <!-- More track elements... --> </track-listing> </cd> <!-- More CDs... --> </cds> |
이제, 열 개 또는 그 이상의 트랙을 가진 CD를 검색해보자. 먼저, 모든 cd 요소를 선택하는데, 지금까지 여러 번 언급한 /cds/cd
경로가 필요하다. 그리고 술어를 사용하면 이미 선택한 노드들 중에서 특정 노드의 개수를 얻을 수 있다. 이 예에서는, track-listing
안에 있는 모든 track
요소 노드를 선택하면 된다. 마지막으로 XPath에서 count()
함수를 사용하여 노드들의 수, 이 예에서는 10과 비교하면 된다. 이 모든 과정을 하나로 만든다면 다음과 같은 경로와 술어가 된다: /cds/cd[count(track-listing/track) >= 10]
.
주의 깊게 읽었다면, XPath가 요소 텍스트와 속성을 다루는 방법에 있어서 약간의 모순점을 발견했을 것이다. 앞에서 속성 노드는 속성과 그 속성 값 모두를 의미하고, 이것은 하나의 정보로 취급된다고 설명했다. 그러나 술어에서 @type
과 같은 표현식은 type
속성 노드 전체를 나타내는 것이 아니라 그 속성의 값을 나타낸다. 그래서 @type='reggae'
처럼 값을 다른 것과 비교할 수 있다.
같은 방식으로, 다음과 같은 술어로 텍스트 요소를 나타낼 수 있다: /cds/cd[title='Revolver']
. 여기서 cd
안에 있는 type
요소의 값은 "Revolver" 값과 비교된다. 속성 노드의 경우처럼 요소에 대한 표준 규칙들에 위배된다. 일반적으로 요소 노드는 텍스트 노드의 부모다. 그러나 지금 말한 것처럼 술어는 요소 안에 있는 텍스트를 나타내지 않는 경우도 있다.
규칙에 대한 이런 미세한 변형은 요소와 속성을 정확히 이해하고 있다면 큰 문제가 되지 않는다. 단지 속성과 그 속성의 값을 하나의 노드로 취급할 경우와 속성의 값을 나타낼 경우만 구분하면 된다. 비슷한 개념으로, 요소가 다른 텍스트 노드를 포함하는 경우와 요소의 텍스트를 다른 값과 비교할 경우를 구분하면 된다.
|
XPath는 놀라울 정도로 강력하지만 제약 사항도 조금 있다. 무엇보다도 XPath는 정적 데이터에 아주 적합해 술어와 XPath를 사용하면 요소, 속성, 텍스트를 특정 데이터와 비교하는 특정 문서를 위한 XPath 쿼리를 만들 수 있다. 또한, XPath는 어떤 제어 구조(if/else 문)도 갖고 있지 않으며, 단순한 비교 이상의 복잡한 처리를 할 수 없다.
엄밀하게 말하면, 이러한 제약사항은 프로그래머가 아닌 대부분의 사람에게는 큰 문제가 되지 않는다. 그러나 자바(또는 C#, 파이썬) 프로그래머들이 XPath를 많이 사용한다면, 그들은 모든 프로그래밍 언어의 능력과 함께 XPath가 단독으로 제공하는 기능을 넘어서 XML 문서를 검색할 수 있는 더 좋은 방법들에 대한 아이디어를 빠르게 내놓을 것이다. 그것이 바로 XQuery가 지향하는 것이다.
XQuery에서 자주 쓰이진 않지만, 가장 중요한 기능은 XPath를 적용할 문서를 지정하는 것이다. 예를 들어, /cds/cd[title='Revolver']
같은 XPath 경로를 적용할 문서를 XQuery의 doc()
함수를 사용해 지정할 수 있다. catalog.xml을 검색한다면, XQuery 표현식인 doc("catalog.xml")/cds/cd[title='Revolver']
를 사용하면 된다.
이 작은 기능 덕분에 프로그램으로 문서를 선택하거나(아마도 사용자가 입력한 값에 따라) 또는 여러 문서를 반복문에 적용해(아마도 네트워크에 있는 모든 iTunes 카탈로그) 각각의 문장에 쉽게 적용하는 코드를 작성할 수 있다.
|
물론, XQuery는 단순히 실행 중에 문서를 선택하는 것 이상의 기능을 제공한다. 이른바 FLWOR을 수행할 때 XQuery는 더 강력한 기능을 제공한다. FLWOR은 "for, let, where, order by, return"의 약자다. XQuery에는 좀 더 정확한 결과를 얻기 위해 사용할 수 있는 구문이 많다.
|
SQL에 익숙한 사람들이라면 좀 더 쉽게 접근할 수 있다. WHERE나 ORDER BY는 SQL 쿼리에서는 널리 쓰이는 부분이다. 그리고 프로그래머들은 for
에 익숙하다. 다음은 FLWOR에서 각 절의 역할에 대한 간단한 설명이다.
for
는 다양한 방법으로 노드 집합의 현재 값을 변수를 할당하므로 그 변수를 조작하면 된다.
where
절에서 XPath보다 더 많은 일을 하지 않으며, 단지 XPath 내에 있는 술어의 위치를 옮겨 놓은 것이다.
order by
절은 데이터를 변경하거나 걸러내지 않고, 결과 값들을 정렬하고, XPath에서 사용하는 위치 값이 아닌 다른 어떤 것을 기준으로 값들을 분류한다.
return
을 사용한다. 각 절에 대해 조금 더 자세히 알아보자.
for
문은 자바와 C# 같은 프로그램 언어에서 사용하는 방법과 거의 비슷하게 쓴다. for 문의 형식은 다음과 같다.
for $variable-name in XPath ... |
변수명은 어떤 식별자(예를 들면 x
)라도 상관없다. 일반적으로 변수명은 용도(firstName
, title
)에 따라 정해지지만, 이 변수는 반복 카운터로만 사용할 것이므로, 문자 한 개만 사용하는 것도 나쁘지 않다.
XPath는 어떤 것이라도 사용할 수 있다. /cds/cd
는 완벽한 예다. 예를 들어 다음과 같이 사용할 수 있다.
for $cd in doc("catalog.xml")/cds/cd ... |
이것이 전부다. 변수 &cd
는 XPath 경로 /cds/cd
에 의해 반환되는 각각의 노드 값을 갖는다. 위 예에서, ...
이라고 표시한 것은 XQuery 표현식의 나머지 부분들인데, 이에 대해서는 뒤에 알아 보겠다.
프로그래머들의 이해를 돕기 위해 예를 들면, 위 문장은 아래 문장과 다를 바 없다.
for (int i = 0; i<cdArray.length; i++) { CD cd = cdArray[i]; // Process CD } |
리스트로 표현해도 비슷하다.
for (Iterator i = cdList.iterator(); i.hasNext(); ) { CD cd = (CD)i.next(); // Process each CD } |
let
절은 변수를 할당하는 데 쓴다. 이미 XQuery에서는 식별자 앞에 달러($
) 표시를 붙임으로써 변수가 정의됨을 알았을 것이다. 대부분의 경우에는 앞에서 설명한 것처럼 XQuery에서는 let
절로 명확하게 변수를 생성하기보다는 for
절에서 묵시적으로 변수를 생성할 것이다.
그러나 명시적으로 변수를 생성하려면 아래와 같이 하면 된다.
let $docName := 'catalog.xml' for $cd in doc($docName)/cds/cd ... |
예제를 보면 검색 대상 문서가 변수에 할당되었다. XQuery는 변수에 할당을 위해 :=
를 사용하는데, 파스칼(Pascal) 언어를 사용해 본 적이 없다면 이 기호가 조금 생소할 것이다. 좀 더 실질적인 상황에서는, XML 문서 목록을 순회하는 함수에서 $docName
에 그 문서들의 이름을 차례로 할당한다. 더 큰 목록에 포함된 각 문서에서 cd
요소를 선택하고, 동일한 방식으로 처리할 수 있다.
FLWOR에서 order by는 잠시 건너 뛰고, 지금은 먼저 쿼리를 완성해보자. 지금까지 설명한 내용은 다음과 같다.
let $docName := 'catalog.xml' for $cd in doc($docName)/cds/cd ... |
다음 단계는 반환이다. 이 쿼리는 모든 cd
요소를 선택했고, 그것이 검색하려던 노드 집합이긴 하지만, 그대로 반환하면 사용하기 힘들다. 이러한 요소들을 그대로 반환하기보다는 title
요소에 저장된 각 CD의 제목처럼 좀 더 구체적인 데이터를 반환하려고 한다. 이때 return
이 필요하다.
let $docName := 'catalog.xml' for $cd in doc($docName)/cds/cd return $cd/title/text() |
설명하자면, 모든 cd
요소를 선택했고, 각 노드는 $cd
에 차례로 할당된다. 마지막으로 return
절은 요소 그 자체를 반환하는 대신, 검색하려던 데이터를 가지고 있는 "title"이라는 이름의 자식 요소를 반환한다.
아래와 같은 단순한 실수를 하지 않도록 주의하라.
let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd
return /cds/cd/title/text()
|
위 예제는 세 가지 중대한 문제를 갖고 있다.
doc($docName)
에 상관없이 데이터를 반환한다.
for
절로 반환된 노드 집합에 적용한 필터나 순서를 무시한다. 다음 작업을 하려고 한다면 이 점은 매우 중요하다. for
절에서 정의하고, return
절에서 다시 사용할 수 있는 변수가 있다는 점을 명심해라. 이 단순하면서도 중요한 법칙을 잘 지키면, 쿼리가 의도한 대로 수행됨을 확신할 수 있다.
where
절을 사용하면 XQuery의 검색 조건을 더욱 세밀하게 지정할 수 있다. XQuery의 where
절은 SQL에서와 같이 동작한다. 결과 집합을 정제하려면 검색에 where
절을 사용하면 된다. 간단한 예제를 보자.
let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd
where $cd@type = 'reggae'
return $cd/title/text()
|
위의 예제는 title이 reggae인 모든 CD를 반환한다. where
절이 꽤 직관적이어서 따로 설명할 것도 없지만, and
와 함께 사용하면 좀 더 복잡한 조건을 적용할 수 있다.
let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd
where $cd@type = 'reggae'
and count($cd/track-listing/track) > 10
return $cd/title/text()
|
위 예제는 열 개 이상의 track을 가진 모든 reggae CD를 반환한다.
where
절의 또 하나의 중요한 용도는 조인(join)을 수행하는 것이다. Listing 2와 같은 XML 파일이 있다고 가정하자.
<cds> <cd style="some-style"> <title>CD title</title> <artist id="289" /> <track-listing> <track>Track title</track> <!-- More track elements... --> </track-listing> </cd> <!-- More CDs... --> <artists> <artist id="289"> <firstName>Bob</firstName> <lastName>Marley</lastName> </artist> <!-- More artist elements --> </artists> </cds> |
위 예제는 Listing 1의 XML 구조를 확장한 것인데, id
속성으로 식별할 수 있는 artist
요소가 추가되었고, 각각의 cd
요소에 포함된 한 개 이상이 artist
요소에 의해 각 CD와 조인되어 있다.
XQuery를 사용하면, CD와 그 CD의 artist를 조인할 수 있다. 아래의 예를 보자.
let $docName := 'catalog.xml' for $cd in doc($docName)/cds/cd, $artist in doc($docName)/cds/artists/artist where $cd/artist/$id = $artist/$id and $artist/lastName = 'Marley' return $cd/title/text() |
여기에서 눈 여겨 볼 것은 두 가지다. 첫 번째로, for
절은 한 개 이상의 변수를 정의할 수 있다. 단지 CD들을 식별하는 대신, 여기서는 $artist
도 정의해 artist
요소를 다루기 쉽다.
다음으로, where
절은 CD와 그 CD의 artist를 조인한다: where $cd/artist/$id = $artist/$id
. 이렇게 하면, 각각의 CD와 그 CD의 artist들이 연결되고, SQL의 join과 같은 결과를 가져온다는 것을 기억하라. 다음으로, 검색 조건을 더 정의했다: $artist/lastName = 'Marley'
. 이렇게 하면, "Marley"라는 lastName을 가진 모든 artist를 검색한다. 조인을 한 다음, return
절로 CD의 title들을 반환한다. 그 결과, lastName이 "Marley"인 artist를 포함하는 모든 CD의 title을 얻는다.
이 예제를 통해 XQuery를 어디에 써먹을지 알 수 있을 것이다. (아마도 많은 문서가 고급 검색을 염두에 둔 구조를 갖고 있지 않겠지만) XML 문서에 대해 SQL과 비슷한 복잡한 조인과 선택을 수행할 수 있다.
where
절은 SQL의 WHERE와 거의 비슷하게 동작하지만, order by
절은 SQL의 ORDER BY와 똑같이 동작한다. XPath로 참조할 수 있는 어떤 것으로든 결과를 정렬할 수 있다.
let $docName := 'catalog.xml' for $cd in doc($docName)/cds/cd, $artist in doc($docName)/cds/artists/artist where $cd/artist/$id = $artist/$id and $artist/lastName = 'Marley' order by $cd/release/@date return $cd/title/text() |
위 예에서는, 반환된 CD의 title들을 각 CD의 자식 요소인 release
의 date
속성에 따라 정렬한다. 기본적으로 정렬은 오름차순(ascending)이지만, 원한다면 아래와 같이 명시적으로 지정할 수 있다.
let $docName := 'catalog.xml' for $cd in doc($docName)/cds/cd, $artist in doc($docName)/cds/artists/artist where $cd/artist/$id = $artist/$id and $artist/lastName = 'Marley' order by $cd/release/@date ascending return $cd/title/text() |
물론, 내림차순(descending)으로 정렬할 수도 있다. 아래 예는 최근에 발매된 CD를 가장 먼저 반환한다.
let $docName := 'catalog.xml' for $cd in doc($docName)/cds/cd, $artist in doc($docName)/cds/artists/artist where $cd/artist/$id = $artist/$id and $artist/lastName = 'Marley' order by $cd/release/@date descending return $cd/title/text() |
주의: date
속성을 날짜 타입(date type)으로 인식하는 스키마(schema)나 XQuery 처리기를 사용하지 않는다면, 위 문장은 오류가 발생한다. 더 안 좋은 것은 date를 문자열로 인식해 알파벳 순서로 정렬한다는 사실이다. 거의 모든 현대적인 처리기는 날짜를 인식하기 때문에 별로 신경 쓸 필요는 없다.
한 번에 여러 개의 정렬 조건을 적용할 수 있다. 예를 들어, 위의 예는 "Marley"라는 lastName의 artist를 포함하는 모든 CD를 반환하면서, 발매일 순으로 정렬한다. 아래 표현식은 artist의 이름, 발매일 순으로 정렬한다.
let $docName := 'catalog.xml'
for $cd in doc($docName)/cds/cd,
$artist in doc($docName)/cds/artists/artist
where $cd/artist/$id = $artist/$id
and $artist/lastName = 'Marley'
order by $artist/firstName, $cd/release/@date
return $cd/title/text()
|
모든 결과의 lastName은 같으므로 $artist/firstName
에 앞서 $artist/lastName
으로 정렬할 필요가 없다는 점을 주목하라.
|
자바 환경에서 XQuery를 사용하는 방법에 대한 글을 많이 접했겠지만, XQJ(XQuery API for Java)를 선택하는 프로그래머는 대부분 XPath와 XQuery를 대충 보고 넘어간다. 이제 여기에서 조금 더 배운 기초를 바탕으로 자바 프로그램에 XQuery를 사용해 보자.
XQJ는 썬의 후원 아래, 자바 커뮤니티 프로세스의 일부인 JSR 225(링크는 참고자료를 보라)로 개발되고 있다. 스펙에는 몇몇 다른 벤더(썬, 노키아부터 BEA, 오라클, 인텔에 이르기까지)뿐 아니라 소수의 핵심적인 개인들(서블릿, JDOM, XML로 유명한 Jason Hunter 같은)도 참여하는데, 그 나름대로 특정 데이터베이스 벤더나 XML 제품 제조사에 종속되는 것을 막기 위해 노력하고 있다.
안타깝지만, XQJ에 대한 썬 표준 구현체는 아직 없다. 전문가 그룹에 있는 벤더 대부분은 자사 제품으로 XQJ를 구현하는 회사에서 일하고 있는데, 이는 특정 벤더에 국한된 문제를 다뤄야 한다는 것을 의미한다. 물론, 오랫동안 XML을 다뤄왔던 이들에겐 2000년대 초반에 있었던 XML 파서와 XSL 처리기 전쟁과 별 다를 바가 없다. 적당한 때가 되면, XQJ는 표준화될 것이고, 썬은 자체적으로 XQJ를 구현하거나, XML 파서와 XSL 처리기가 JAXP가 동작하는 것처럼 XQJ 구현을 위한 추상화된 API를 발표할 것이다.
|
XQJ를 시작하는 쉬운 방법은 DataDirect에서 무료 시험판을 다운로드하는 것이다. 양식을 작성하는 게 귀찮겠지만, 이 글을 따라가다 보면 충분한 시간을 벌 수 있다. DataDirect XQuery 다운로드 사이트를 방문하자(참고자료). XML Documents Only 옵션을 선택하고, 접근을 원하는 데이터베이스를 입력해야 한다. 이 옵션들을 선택하면 datadirectxquery.jar 파일을 받을 수 있는 곳에 대한 안내가 들어있는 이메일을 받는다.
설치 과정은 조금 혼란스럽다. 먼저, jar
명령을 사용하여 다운로드한 datadirectxquery.jar의 압축을 풀어야 한다. 그러나 그 전에 먼저, 설치할 디렉터리를 만들고, 그 디렉터리에 JAR 파일을 푼 다음, jar
명령을 실행하자.
[bdm0509:~/Desktop] mkdir xqj [bdm0509:~/Desktop] cd xqj [bdm0509:~/Desktop/xqj] jar xvf ../datadirectxquery.jar inflated: XQueryInstaller.jar inflated: ddxqj.jar inflated: ExtensionTool.jar inflated: Readme.txt inflated: 3rdPartySoftware.txt inflated: Fixes.txt inflated: installer.properties |
이제 조금 전에 만든 디렉터리를 열고 XQueryInstaller.jar 파일을 더블클릭하자. 자바가 설치되어 있다면, GUI 설치 프로그램이 실행될 것이다.
trial 또는 licensed version 중에 선택하는 단계에서, trial을 선택하자. 다음으로, 설치할 디렉터리를 선택해야 한다. 여기에서 입력한 디렉터리에 쓰기 권한이 있는지 확인하자. 이 예에서는 /usr/local/java/xqj를 선택했는데, 먼저 /usr/local/java 디렉터리에 쓰기 권한이 있는지 확인해야 한다. 설치 과정에서 마지막 하위 디렉터리(이 예에서는 xqj)가 만들어지고, 그 안에 DataDirectory XQuery 파일들이 설치된다. 마지막으로 Finish를 클릭하고 설치가 마무리된다.
모두 마쳤으면, 새 디렉터리의 내용을 확인하자. 아래에 보이는 목록과 비슷해야 한다.
[bdm0509:/usr/local/java] cd xqj [bdm0509:/usr/local/java/xqj] ls 3rdPartySoftware.txt examples lib Fixes.txt help planExplain Readme.txt javadoc src |
이제, 클래스패스에 lib 디렉터리와 ddxq.jar에 있는 XQJ 클래스들과 XQuery JAR를 추가해야 한다. 이것은 다운로드했던 원래의 JAR 파일이 아니라, 앞에서 압축을 푼 JAR 파일에 의해 설치된 것이다(DataDirect는 ZIP이나 tar.gz 파일을 다운로드하도록 해서 혼란을 줄이고 있다). 클래스패스는 수동으로도 설정할 수도 있고, 셸 스크립트나 .profile 파일을 사용할 수도 있고, 또는 IDE에서 JAR 파일을 클래스패스에 추가할 수도 있다. 어찌 됐건 ddxq.jar가 클래스패스에 들어있으면 된다.
DataDirect에서 다운로드한 제품은 관계형 데이터베이스에 대해 XQueiry를 실행할 수도 있고, 데이터베이스에 연결할 수도 있다. 다운로드 양식을 작성할 때 어떤 데이터베이스를 사용할 것인지를 꼭 선택해야 한다. 그렇게 해야 데이터베이스 설정이 가능하도록 조정된 파일을 다운로드할 수 있다. 이 부분은 이 글의 범위에서 약간 벗어나지만, DataDirect 라이브러리를 데이터베이스 연결에 사용하고, 디스크에 있는 XML 문서를 XQuery로 만드는 데 관심이 있다면, 참고자료에 있는 관련 링크를 확인해 보면 된다. 설치 디렉터리 아래의 lib 디렉터리에는 다른 JAR 파일이 많이 있지만, 단순한 파일 쿼리에는 필요가 없다. 나중에라도 DataDirect를 데이터베이스 연결에 사용하면, 그 JAR 파일들을 살펴보면 된다.
|
지금까지 XPath와 XQuery에 대해 알아보고, 클래스패스에 XQJ를 추가했으니, 자바 코드를 작성하여 쿼리를 실행할 준비가 끝났다. 다음은 모든 프로그램 작성에서 따라야 하는 두 가지 기본 과정이다.
둘 다 단순한 과정이고, 다른 XQuery 구현체로 변경하지 않는다면, 첫 번째 과정은 모든 프로그램에서 똑같다. 사실, 데이터 소스 설정과 연결을 다루는 코드는 유틸리티 클래스로 만들면 된다(연습 삼아 해보자).
|
JDBC를 많이 사용해봤거나 n-티어 데이터베이스 애플리케이션을 작성해 봤다면 데이터 소스에 대한 개념은 친숙할 것이다. 이 글에서 말하는 데이터 소스는 어떻게 연결이 생성되고, 그리고 연결이 이디에 연결되는지에 대한 상세 정보를 추상화한 연결 객체다. 따라서 데이터 소스가 MySQL 데이터베이스에 네트워크로 연결된 것이나 정적 XML 문서에 파일 기반으로 연결되어 있는 것을 나타낼지도 모른다. 일단 데이터 소스를 가졌으면 연결 시맨틱에 상관 없이 그것을 다룰 수 있다.
로컬 디스크에 있는 XML 문서(이 글에서 다루는)에 쿼리를 수행한다면, 연결 설정은 간단하다. Listing 3은 새로운 데이터 소스를 만들고, 쿼리를 할 수 있는 기본적인 자바 프로그램이다.
package ibm.dw.xqj; import com.ddtek.xquery3.XQConnection; import com.ddtek.xquery3.XQException; import com.ddtek.xquery3.xqj.DDXQDataSource; public class XQueryTester { // Filename for XML document to query private String filename; // Data Source for querying private DDXQDataSource dataSource; // Connection for querying private XQConnection conn; public XQueryTester(String filename) { this.filename = filename; } public void init() throws XQException { dataSource = new DDXQDataSource(); conn = dataSource.getConnection(); } public static void main(String[] args) { if (args.length != 1) { System.err.println("Usage: java ibm.dw.xqj.XQueryTester [XML filename]"); System.exit(-1); } try { String xmlFilename = args[0]; XQueryTester tester = new XQueryTester(xmlFilename); tester.init(); } catch (Exception e) { e.printStackTrace(System.err); System.err.println(e.getMessage()); } } } |
위 프로그램은 실제보다 길고 복잡해 보인다. 테스트 프로그램은 생성자와 init()
메서드 등으로 잘 모듈화되어 있으므로 수정 없이 쉽게 재사용할 수 있다.
이 프로그램은 파일 이름을 받아(명령행으로 받아서 클래스 생성자에 전달되는), 아래 코드를 실행한다.
dataSource = new DDXQDataSource(); conn = dataSource.getConnection(); |
먼저, 새로운 데이터 소스가 만들어진다. 이 객체의 타입은 com.ddtek.xquery3.xqj.DDXQDataSource
다. 데이터베이스에 연결하지 않기 때문에 인자가 없는 생성자를 사용하고, 다른 환경 설정도 필요 없다. 이렇게 만들어진 데이터 소스는 새로운 com.ddtek.xquery3.XQConnection
객체를 얻는 데 사용된다. 이 객체는 새로운 XQuery 표현식을 생성하고 실행하는 중요한 역할을 한다. 이제 쿼리를 실행하는 프로그램을 만들어보자.
실제로 쿼리를 수행하려면 XML 파일이 필요하다. 다운로드에서 zip으로 압축해 놓은 예제 CD 카탈로그 파일을 찾을 수 있을 것이다. 이 XML 문서는 W3C에서 예제로 제공하는 것이다. 200줄이 넘는 문서를 여기서 다 보여줄 필요는 없겠지만, 쿼리를 수행하기에 좋은 문서다.
Listing 4는 문서의 구조를 알 수 있는 일부분이다.
Listing 4. cd_catalog.xml의 일부분
<?xml version="1.0" encoding="ISO-8859-1"?> <CATALOG> <CD> <TITLE>Empire Burlesque</TITLE> <ARTIST>Bob Dylan</ARTIST> <COUNTRY>USA</COUNTRY> <COMPANY>Columbia</COMPANY> <PRICE>10.90</PRICE> <YEAR>1985</YEAR> </CD> <CD> <TITLE>Hide your heart</TITLE> <ARTIST>Bonnie Tyler</ARTIST> <COUNTRY>UK</COUNTRY> <COMPANY>CBS Records</COMPANY> <PRICE>9.90</PRICE> <YEAR>1988</YEAR> </CD> <!-- more CD listings ... --> </CATALOG> |
다음으로 쿼리를 만들어야 한다. 단순한 자바 String
을 사용하면 된다. 새로운 변수를 만들고, 아래 예제와 같이 XQueryTester
클래스의 main()
메서드에 쿼리할 문자열을 만들자.
try {
String xmlFilename = args[0];
XQueryTester tester = new XQueryTester(xmlFilename);
tester.init();
final String sep = System.getProperty("line.separator");
String queryString =
" for $cd in doc($docName)/CATALOG/CD " +
" where $cd/YEAR > 1980 " +
" and $cd/COUNTRY = 'USA' " +
" order by $cd/YEAR " +
" return " +
"<cd><title>{$cd/TITLE/text()}</title>" +
" <year>{$cd/YEAR/text()}</year></cd>";
System.out.println(tester.query(queryString));
} catch (Exception e) {
e.printStackTrace(System.err);
System.err.println(e.getMessage());
}
|
쿼리는 1981년부터 US label에서 녹음된 모든 CD를 선택해 발매일 순서로 정렬한다. 그리고 반환하는 각 CD의 제목과 발매일을 포함한 XML을 반환한다. 따로 설명할 만한 것이 별로 없다.
docName
은 검색 대상 문서를 나타낸다. 명령행으로 지정한 문서를 사용한다.
CD
, TITLE
, YEAR
등은 결과 집합에서는 사용되지 않으며, cd
, title
, year
같은 새로운 요소 이름을 사용한다. 별로 어려운 부분도 없고, 단지 XQuery를 사용하여 XML로부터 데이터를 선택해 반환하는 데이터를 반환하는 유연한 방법을 보여주는 예다.
특이한 부분은, 결과 집합으로 XML 문자열을 반환할 때, 변수를 중괄호({ })로 감싸야 한다는 점이다.
return <cd><title>{$cd/TITLE/text()}</title>" + " <year>{$cd/YEAR/text()}</year></cd>" |
이 중괄호는 XQuery 처리기에게 문자열 그대로 사용하지 말고, 괄호 속에 있는 값을 변수 이름으로 인식해서, 해당 변수의 값으로 대치하도록 한다.
XQuery는 문서를 나타내는 데 $docName
같은 변수를 사용한다. 그러나 명시적으로 변수를 선언해야 할 필요가 있고, 외부, 여기서는 자바 프로그램에서 그 변수를 정의했음을 알려야 한다. XQuery에서는 declare
구문을 사용하는데, 그 형식은 다음과 같다.
declare variable [variable-name] as [variable-type] external; |
이 예에서는, [variable-name]은 $docName
이며, [variable-type]은 XML 스키마의 데이터형이다. 대부분 문자열은 xs:string
을, 정수는 xs:int
를 사용하면 된다. 다른 여러 가지 데이터형이 있지만, 별로 쓸 일이 없다.
외부 변수 선언을 포함하도록 XQueryTester
클래스의 쿼리를 수정하자.
try {
String xmlFilename = args[0];
XQueryTester tester = new XQueryTester(xmlFilename);
tester.init();
final String sep = System.getProperty("line.separator");
String queryString =
"declare variable $docName as xs:string external;" + sep +
" for $cd in doc($docName)/CATALOG/CD " +
" where $cd/YEAR > 1980 " +
" and $cd/COUNTRY = 'USA' " +
" order by $cd/YEAR " +
" return " +
"<cd><title>{$cd/TITLE/text()}</title>" +
" <year>{$cd/YEAR/text()}</year></cd>";
System.out.println(tester.query(queryString));
} catch (Exception e) {
e.printStackTrace(System.err);
System.err.println(e.getMessage());
}
|
이제 남은 것은 이 쿼리를 실행할 함수를 만드는 것이다.
쿼리를 수행하기 위해서 다음 세 단계를 수행해야 한다.
XQConnection
에서 XQExpression
을 생성한다.
XQExpression
객체의 bindXXX()
메서드를 사용해 쿼리와 변수를 결합한다.
XQSequence
객체에 저장한다. 까다로운 부분은 변수를 결합하는 단계뿐이다. 이 예에서, 변수 docName
은 프로그램에 전달되는 파일 이름과 결합해야 한다. 문자열 변수를 결합하기 위해 bindString
메서드를 사용하는데, 이 메서드는 세 개의 인자가 필요하다.
javax.xml.namespace.QName
인스턴스(이 클래스는 JAXP 패키지에 있다)
위의 내용을 정리하면, 다음과 같은 메서드가 된다.
public String query(String queryString) throws XQException { XQExpression expression = conn.createExpression(); expression.bindString(new QName("docName"), filename, conn.createAtomicType(XQItemType.XQBASETYPE_STRING)); XQSequence results = expression.executeQuery(queryString); return results.getSequenceAsString(new Properties()); } |
첫 번째 줄의 XQExpression expression = conn.createExpression();
은 새로운 표현식 객체를 만든다. expression.bindString(new QName("docName"), filename, conn.createAtomicType(XQItemType.XQBASETYPE_STRING));
은 검색 대상 문서의 파일 이름과 docName
변수를 결합한다. QName
객체에 대해서는 너무 걱정할 필요가 없다. 그냥 XQuery에 있는 변수 이름($
문자를 제외하고)을 전달하면 된다. 두 번째 인자는 파일 이름이고, 세 번째 인자는 변수의 자료형을 나타내는 상수 값이다. 이 예에서는 쿼리의 변수가 xs:string
으로 선언되어 있으므로 XQItemType.XQBASETYPE_STRING
을 사용했다.
직관적으로 이해가 되지 않는다면, API 문서에서 더 자세한 설명을 볼 수 있다(링크는 참고자료를 보라). bindInt()
, bindFloat()
등의 다른 bindXX()
메서드들도 같은 방식으로 동작한다.
마지막으로 쿼리를 실행하고 결과 집합을 넘겨 받는다. 이 결과 집합은 순회(iterate)할 수 있다. query()
메서드를 사용하면 문자열로 변환할 수도 있는데, 이렇게 하면 호출하는 프로그램이 DataDirect XQuery API에 대해 알 필요가 없다.
Listing 5는 완성된 XQueryTester
의 코드다.
Listing 5. 완성된 XQueryTester 클래스
package ibm.dw.xqj;
import javax.xml.namespace.QName;
import java.util.Properties;
import com.ddtek.xquery3.XQConnection;
import com.ddtek.xquery3.XQException;
import com.ddtek.xquery3.XQExpression;
import com.ddtek.xquery3.XQItemType;
import com.ddtek.xquery3.XQSequence;
import com.ddtek.xquery3.xqj.DDXQDataSource;
public class XQueryTester {
// Filename for XML document to query
private String filename;
// Data Source for querying
private DDXQDataSource dataSource;
// Connection for querying
private XQConnection conn;
public XQueryTester(String filename) {
this.filename = filename;
}
public void init() throws XQException {
dataSource = new DDXQDataSource();
conn = dataSource.getConnection();
}
public String query(String queryString) throws XQException {
XQExpression expression = conn.createExpression();
expression.bindString(new QName("docName"), filename,
conn.createAtomicType(XQItemType.XQBASETYPE_STRING));
XQSequence results = expression.executeQuery(queryString);
return results.getSequenceAsString(new Properties());
}
public static void main(String[] args) {
if (args.length != 1) {
System.err.println("Usage: java ibm.dw.xqj.XQueryTester [XML filename]");
System.exit(-1);
}
try {
String xmlFilename = args[0];
XQueryTester tester = new XQueryTester(xmlFilename);
tester.init();
final String sep = System.getProperty("line.separator");
String queryString =
"declare variable $docName as xs:string external;" + sep +
" for $cd in doc($docName)/CATALOG/CD " +
" where $cd/YEAR > 1980 " +
" and $cd/COUNTRY = 'USA' " +
" order by $cd/YEAR " +
" return " +
"<cd><title>{$cd/TITLE/text()}</title>" +
" <year>{$cd/YEAR/text()}</year></cd>";
System.out.println(tester.query(queryString));
} catch (Exception e) {
e.printStackTrace(System.err);
System.err.println(e.getMessage());
}
}
}
|
이 프로그램을 컴파일하고 실행하자.
[bdm0509:~/Documents/developerworks/java_xquery] java ibm.dw.xqj.XQueryTester cd_catalog.xml <cd><title>Greatest Hits</title><year>1982</year></cd><cd><title> Empire Burlesque</title><year>1985</year></cd><cd><title>When a man loves a woman </title><year>1987</year></cd><cd><title>The dock of the bay</title><year> 1987</year></cd><cd><title>Unchain my heart</title><year>1987</year></cd> <cd><title>Big Willie style</title><year>1997</year></cd><cd><title> 1999 Grammy Nominees</title><year>1999</year></cd> |
주의: 온라인에서 보일 것으로 고려하여 줄바꿈을 삽입했지만, 실제 결과는 줄바꿈 없이 한 줄에 모두 나타날 것이다.
위 프로그램은 쿼리를 처리하여 지정한 XML 문서에 적용하도록 되어 있다. 이제 쿼리 문자열을 변형해 가면서 실행해 볼 수 있고, 그렇게 하면서 XQuery와 자바로 쿼리를 수행하는 과정에 대해 좀 더 이해할 수 있을 것이다. 모든 CD를 선택해보자. 또, 반환되는 결과 형식을 정형화된 텍스트로 바꿔보자. 최근 것부터 가장 오래된 것 순으로 모든 CD 목록을 볼 수도 있고, 가격이 10달러 이하인 모든 CD를 찾아볼 수도 있다. 한번 이렇게 프로그램을 작성해 두면, 쿼리를 조정하고, 시험하고, 프로그램에서 사용할 XML 문서를 변형하는 일을 더 쉽게 할 수 있다.
|
XQuery의 기초를 얘기하지 않고 XQJ에 대해 설명하기란 쉬운 일이 아니다. 또한, XPath를 깊이 있게 설명하지 않고는 XQuery에 대해 설명할 수도 없다. 이 모든 것을 하나로 연결하는 것은 단지 한 가지를 — 쿼리를 수행하는 자바 프로그램, 또는 쿼리를 시작할 문서 내의 위치 — 더 쉽게 하기 위해서다. 자바에서 XQuery 사용을 고려할 때, 가장 좋은 방법은 개별적인 단순한 부분을 모아 멋지고 강력한 프로그램을 만드는 것이다.
여러 다른 기술에 친숙해지려면 두 개의 컴포넌트는 변형하지 말고, 세 번째 것을 변형하여 실행하면 된다. 즉, 자바 프로그램은 수정하지 말고, 쿼리와 검색 대상 문서만 바꿔가면서 실행해보자. 그리고 기본적인 쿼리도 수행해보고, 선언된 변수가 더 많은 복잡한 쿼리도 시험해보자. 아마 return 문과 검색의 시작 위치에 XPath를 적용하고 싶어질 것이다. 각 컴포넌트에 능숙해질수록, 쿼리들은 더 복잡해지고, 자바 코드도 더욱 견고해진다.
|