난이도 : 초급
Benoit Marchal, 컨설턴트, Pineapplesoft
2002 년 1 월 01 일
2004 년 4 월 05 일 수정
SAX
ContentHandler
컴파일러인 HC에 대한 작업이 계속되고 있다. 이번 달에 우리의 컬럼니스트는 컴파일 알고리즘을 설명하며, 또한 JUnit로 테스트를 자동화하는 사항도 다룬다.
Beniot Marchal은 Working XML 컬럼에서 매달 디자인 결정에서부터 코딩의 과제까지 XML 개발자들을 위한 그의 오픈 소스 프로젝트의 진행 상황을 설명하였다. HC (Handler Compiler의 준말)라고 불리는 이 새로운 프로젝트는 XPaths 목록에 대해 SAX ContentHandler를 자동으로 생성함으로써 이벤트 지향의 XML parsing에서 몇 가지 힘든 일을 없애줄 것이다.
핸들러 컴파일러인 HC에 대한 작업이 계속되고 있다. 지난 달 이 컬럼에서 소개된 것처럼, HC의 목표는 SAX ContentHandler
의
상태를 추적할 필요를 없애는 것이다. 상태 추적은 지루하고 에러가 나기 쉬운 작업이다. HC는 프록시 ContentHandler
를
컴파일함으로써 이 절차를 자동화하는데, 프록시 ContentHandler
는 상태 관리와 애플리케이션 로직에
적합한 애플리케이션 핸들러로의 호출을 처리한다.
언뜻 보면 HC가 프로그래머에게 더 많은 작업을 요구하는 것처럼 보이지만, 프록시가 애플리케이션 핸들러로부터 자동으로 컴파일된다는 점을 이해하는 것이 중요하다. 어떤 프로래밍도 필요하지 않다.
나는 컴파일러 구축에 있어 가장 확실한 참조 자료 중 하나인 Compilers: Principles, Techniques and Tools (참고 자료)로부터 대부분의 알고리즘 구성 요소를 가져올 것이다. 여러분 가까이에 한 권이 있다면, DFA 구축은 알고리즘 3.5이다.
지난 달의 설명에서 DFA가 변환 (transition) 다이어그램이라는 사실을 회상해보자. 그림
1은 simpara/ulink
XPath의 변환 다이어그램이다. 원은 프록시가 갈 상태들을 나타내며,
화살표는 DFA 변환을 가져오는 요소들을 지칭하고 있다. 굵은 선으로 된 원은 XPath가 성공적으로 인식되었음을 나타낸다.
알고리즘을 더 잘 이해하기 위해 여러분은 이 변환 다이어그램을 스택과 비교할 수 있다. 본질적으로 프록시는 simpara
element를 만났을 때 이를 스택에 둔다. 다음에 ulink
를 만나면 이것 역시 스택에 둔다. 스택은
이제 simpara
와 ulink
라는 두 element를 가지고 있는데, 이것은
simpara/ulink
구성이다.
다이어그램의 다양한 단계는 스택의 구성을 나타낸다. 상태 0은 빈 스택을, 상태 1은 한 element (simpara
)를
가진 스택을 표시한다. 상태 2는 두 element (simpara
와 ulink
)을
가진 스택이다.
다시 말해, DFA를 구축하려면 여러분은 가능한 스택 구성만큼 많은 상태를 할당해야 하고 이 상태들간의 변환 기능을 산출해야 한다.
여러분이 상상할 수 있듯이, 그림 1은 너무 간단하다. 실제 프록시는 하나가 아닌 여러 개의 XPaths를 인식하려 할 것이기 때문에 여러 변환 다이어그램을 병행하여 처리할 것이다. 예를 들어, 프록시는 다음 세 XPaths 중 하나를 찾을 것이다.:
simpara/ulink |
DFA가 더 많은 XPaths를 인식하려고 시도하면 상태 번호가 증가할 것이다. 실제로, 스택 측면에서 생각하면 simpara/ulink
를
인식하는 것보다 이들 세 XPaths를 인식하기 위해 더 많은 스택 구성이 있을 것이다..
XML elements는 이름 공간 URI와 지역명(local name)이 결합된 것으로 식별된다. 이 결합을 좀 더 효과적으로
처리하기 위해 나는 QName
클래스를 만들었다. XML 노드를 완벽하게 식별하기 위해 QName
은
또한 노드의 특성 (element, 속성, 혹은 루트(/))를 기록한다. QName
의 코드는 Listing
1에 있다.
QName
은 해시 테이블과 호환하기 위해 equals()
과 hashCode()
를
구현한다.
알고리즘은 XPaths 세트를 처리하고 세 가지를 컴파일한다. 변환 다이어그램이 거칠 상태들, 한 상태에서 다른 상태로 이동하는 변환 기능, 그리고 어떤 상태가 Xpath를 성공적으로 인식했는지를 나타내는 표시가 그것이다.
알고리즘은 프런트 엔드 (프런트 엔드와 백 엔드에 대한 상세 사항은 이전
컬럼 참조)가 인식할 모든 XPaths를 포함한 parse 트리를 반환할 것이라고 기대한다.
Listing 2는 트리에 나타난 element들인 HCNode
이다. 그러나 이 목록은 HC 개발의
현 단계에서는 불완전하다는 점에 주의한다.; 다음 컬럼에서 좀 더 완성된 버전을 보여주겠다.
HCNode
는 컴파일러에 특화되어 있기 때문에 org.ananas.hc.compiler 패키지에
있다. QName
은 org.ananas.hc
에 있는데, 프록시가 이를 이용할 것으로 기대하기
때문이다.
노드는 (자신의 type
속성에 따라) 다음 중 하나가 될 수 있다.:
XML 노드는 QName
에 대한 참조를 가지고 있다. 다른 노드 유형은 트리를 구축하기 위해 왼쪽과
오른쪽 HCNode
s에 대한 참조를 가지고 있다.
이전 컬럼에서 설명한대로, DFA 구축 알고리즘은 이 parse 트리를 상태 세트로 변환한다. 이를 위해 알고리즘은 얼마나 많은 XML 노드가 주어진 노드 뒤에 나타나는지를 계산한다. 각 상태가 특정 스택 구성을 나타낸다는 점을 기억하면 알고리즘의 이 부분을 이해하기가 더 쉬울 것이다. 알고리즘은 본질적으로 parse 트리 내의 주어진 노드에 이를 수 있는 모든 가능한 스택 구성을 산출한다.
이를 위해, HCNode
는 다음 메소드들을 제공한다. :
n.firstpos()
: n
노드에 있는 XPath의 첫번째 element와
매치되는 XML 노드 세트 n.lastpost()
: n
노드에 있는 XPath의 마지막 element와
매치되는 XML 노드 세트 n.nullable()
: n
이 공백이 될 수 있는 XPath의 루트일
경우에는 참, 그렇지 않으면 거짓이다. 두 가지 예를 들어 보겠다. XPath simpara
로 출발해 보자. 이 XPath는 하나의 노드,
XML_NODE
유형의 n
으로 parsing될 것이다. 이 노드의 n.firstpos()
과
n.lastpos()
는 simpara
가 될 것이다. simpara
rk가
XPath simpara
에 매치되는 유일한 XML 노드이기 때문이다.
XPath simpara/ulink
는 각각 simpara
와 ulink
를
가리키는 좌우 노드와 함께 PARENT_OF
유형의 n
노드로 parsing될
것이다. XPath의 시작과 매치되는 XML 노드가 simpara
이고 XPath의 끝과 매치되는 노드가
ulink이기 때문에,
n.firstpos()
메소드는 simpara
이고
n.lastpost()
는 ulink
이다.
표 1은 firstpos()
과 nullable()
을
산출하기 위한 규칙을 설명한다. lastpos()
은 firstpos()
과 유사하지만,
left
와 right
에 대한 규칙이 반대이다. 여러분은 nullable이 모두
무엇을 가리키는지 궁금할 수도 있다.; 이것은 다음 컬럼에서 상대 XPaths 처리를 설명할 때 더 명확해질 것이다.
표 1.firstpos()와 nullable() 산출하기
firstpos() | nullable() | |
n이 상대 XPath를 표시한다. | empty set | 참 |
n이 XML 노드이다. | { qname } | 거짓 |
OR_XPATH | left.firstpos() U right.firstpos() | left.nullable() or right.nullable() |
PARENT_OF | if left.nullable() then left.firstpos() U right.firstpos() else left.firstpos() | left.nullable() and right.nullable() |
혼란스럽다면, 상태들이 스택 구성을 나타낸다는 사실만 기억하도록 한다. firstpos()
과 lastpos()
는
주어진 스택 구성에 있는 XML 노드 세트이다. 결국 나는 하나의 상태로 변할 유일한
숫자를 이 세트들 각각에 할당할 것이다.
parse 트리가 주어진다면, 여러분은 변환 기능을 처리하기 위해 Listing 3의 알고리즘들을
적용한다. 여러분은 변환 기능을 이차원 행렬인 dtran
으로
나타낼 수 있는데, dtran
은 주어진 XML 노드에게 다음 단계를 반환한다. Listing 3의 알고리즘은
실제 자바 코드가 아닌 의사 코드임에 유의한다.
dstates <- root.firstpos() |
나는 이번 컬럼에서 이 알고리즘을 완전하게 구현하고 싶었지만, JUnit를 배우는데 얼마간의 시간을 투자해야 했다. JUnit는 자동화된 테스팅을 위한 오픈 소스로 된 프레임워크이다 (참고 자료). 나는 컴파일러를 작성할 때 자동화된 테스팅이 절대적으로 필요하다는 사실을 어렵게 알아 냈다.
extreme programming 운동 (참고 자료)은 자동화된 테스트를 보급시켰다. 고백컨대, 나는 자동화된 테스트를 체계적으로 사용하지 않는다. 내 경험에 따르면 자동화된 테스트는 public 인터페이스가 자주 변경되지 않는 클래스에서 가장 효과적이었다. public 인터페이스가 많이 변경되는 클래스에서는 덜 효과적이었고, 여기에는 대부분의 사용자 인터페이스 코드가 포함된다.
자동화된 테스팅의 연산어는 automated이고, 여러분이 자주 사용하지 않을 무언가를 자동화하는 것은 쓸모없는 일이다. 마찬가지로 여러분이 자주 실행시킬 것이라고 알고 있는 것에 대한 테스트를 작성하는 것이 효과적이다. 자동화된 테스트는 또한 트리 조작과 같은 다소 애매한 코드와 특히 컴파일러에 이상적이다.
자동화된 테스팅이 전혀 수고를 필요로 하지 않고 고통도 없다는 바보같은 생각은 하지 말기 바란다. 여러분은 테스트 케이스 작성에 시간을 투자해야 하고, 물론 테스트 애플리케이션을 정기적으로 실행시켜야 한다. 실제로 내가 자동화된 테스트에 그렇게 많은 시간을 쏟아 붓지 않았다면 나는 이번 달에 버그 투성이지만 잘 테스트되지 않은 DFA 컴파일러 버전 작성을 끝낼 수 있었을 것이다. 나는 내가 할 수 있는 것보다 더 많은 시간을 자동화된 테스트에 사용했는데, 이는 내가 JUnit 학습을 시도했기 때문이다.
여전히, 자동화된 테스팅은 프로젝트 기간동안 기대했던 성과를 올리는 투자이다. 테스트를 자동화시키려고 노력하는 것보다 테스트를 바로 수행하는 편이 빠르다. 예를 들어, 주어진 클래스를 수작업으로 한 번 테스트하는 것보다 테스트 케이스를 작성하는데 5시간의 작업이 더 필요할 수 있다. 물론 여러분이 테스트를 6번 이상 실행시킨다면 테스트 케이스를 작성하는 것이 수지가 맞을 것이다. 가령 여러분이 테스트를 50번 실행시키면 (이 횟수는 중간 규모의 프로젝트에서조차 많은 것이 아니다), 그 효과는 엄청나다.
실질적으로 말해, 자동화된 테스트를 작성하기 위해서는 두 가지 전략 중 하나를 따를 수 있다. 테스팅되는 코드를 작성할 때 테스트를 작성할 수도 있고 테스트를 작성을 전담하는 또다른 개발자 팀을 만들 수도 있다. 당분간은 내가 혼자 테스트를 작성하겠지만, 여러분이 테스트 suite 작성을 자원하고 싶다면 ananas-discussion 메일링 리스트에 등록하기 바란다.
과거에 나는 내 테스트를 평이한 자바 클래스로 작성하였다. 그러나 한 친구가 Junit을 테스트해 보라고 제안하였다. HC는 JUnit을 테스트하기에 좋은 프로젝트로 보였으므로, 나는 이것을 다운로드하였다. JUnit은 자동화된 테스트를 작성하기 위한 간단하고 작은 프레임워크이다. JUnit은 그렇게 많은 기능을 가지고 있지 않지만 (단지 몇 개의 클래스일 뿐이다), 테스팅 절차를 공식화하는 것을 도와준다.
Listing
4는 내가 QName
을 연습하기 위해 작성한 테스트 절차이다. 여러분이 볼 수 있듯이, 테스트
클래스는 JUnit가 정의한 클래스인 TestCase
로부터 상속된다. 나는 내 테스트를 testXXX()
메소드에 작성하였다. 나는 소프트웨어의 다양한 측면을 연습하기 위해 세 가지 메소드를 가지고 있다.: 생성자 테스팅, getters
테스팅 및 equals()
와 hashCode()
메소드 테스팅이 그것이다. JUnit에
의해 선언되는 setUp()
메소드는 테스트 객체를 초기화시킨다.
나는 내 테스트들을 별개의 패키지(org.ananas.hc.test
) 에 두기로 하였는데, 이는 Junit
저작자의 권고와 반대되는 것이었다. 이 선택을 후회할지도 모르지만, 나는 실제 코드에서 테스트를 분리하는 것을 선호한다. 배포판에서
테스트를 제거하는데 더 쉽기 때문이다.
JUnit을 사용할 때의 이점은 프레임워크가 테스트 클래스 로딩, 테스트 실행 및 결과 보고를 지원한다는 점이다. 프레임워크는 또한 테스트 suite라는 개념을 지원하는데, 이는 테스트를 논리적 단위로 조직화할 수 있도록 한다. 마지막으로, 프레임워크는 그래픽으로 된 실행자와 콘솔 실행자를 제공한다. 그래픽 콘솔은 한두개 클래스들을 대화식으로 테스트하는데 좋은 반면, 콘솔은 예를 들어 전체 재빌드의 일부와 같은 배치 모드로 테스트를 실행시킨다.
![]() |
![]()
|
나는 아직 HC의 코드를 ananas.org 레포지토리에 게시하지 않았는데, 완전한 준비가 되지 않았기 때문이다. DFA 컴파일을 마무리해야 게시할만한 가치가 있는 새로운 것을 가질 수 있을 것이다. 그러나 자동화된 테스트에 (그리고 JUnit에 관해 공부하는데) 투자된 시간은 잘 사용되었다고 확신한다. ; 그 시간은 이번 프로젝트 동안 보답될 것이다. 다음 달에는 컴파일러의 실행 버전을 (최적화되지 않은 버전이긴 하지만) 가질 수 있기를 바라며, 프록시에 대해 작업할 계획이다.
![]() | ||
![]() | Benoit Marchal 은 벨기에 Namur에 기반을 둔 컨설턴트겸 저작자이다. 그는 최근 XML by Example, second edition을 출간하였다. 그는 또한 Applied XML Solutions과 |
SSISO Community