IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

Working XML: 경로 컴파일과 테스트 자동화
목 차:
프록시 컴파일하기
JUnit
다음 편에는
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
Working XML: HC 소개
SAX, 강력한 API
US 원문 읽기
알고리즘과 JUnit 자세히 살펴보기


Benoit Marchal
컨설턴트, Pineapplesoft
2002년 1월

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가 프로그래머에게 더 많은 작업을 요구하는 것처럼 보이지만, 프록시가 애플리케이션 핸들러로부터 자동으로 컴파일된다는 점을 이해하는 것이 중요하다. 어떤 프로래밍도 필요하지 않다.

프록시 컴파일하기

지난 달에 설명했듯이, 나는 Deterministic Finite Automaton (DFA)를 사용하여 프록시를 컴파일할 계획이다. DFA는 매우 효율적이기 때문에 매력적이다. 또한 이들을 구축하기 위한 알고리즘이 잘 문서화되어 있다. 내가 사용할 알고리즘은 원래 정규식을 컴파일하기 위해 설계되었지만, 나는 이것이 XPaths에도 쉽게 적용될 수 있다고 믿는다.

나는 컴파일러 구축에 있어 가장 확실한 참조 자료 중 하나인 Compilers: Principles, Techniques and Tools (참고 자료)로부터 대부분의 알고리즘 구성 요소를 가져올 것이다. 여러분 가까이에 한 권이 있다면, DFA 구축은 알고리즘 3.5이다.

알고리즘의 기본

지난 달의 설명에서 DFA가 변환 (transition) 다이어그램이라는 사실을 회상해보자. 그림 1simpara/ulink XPath의 변환 다이어그램이다. 원은 프록시가 갈 상태들을 나타내며, 화살표는 DFA 변환을 가져오는 요소들을 지칭하고 있다. 굵은 선으로 된 원은 XPath가 성공적으로 인식되었음을 나타낸다.

그림. simpara/ulink의 변환 다이어그램
Transition diagram

알고리즘을 더 잘 이해하기 위해 여러분은 이 변환 다이어그램을 스택과 비교할 수 있다. 본질적으로 프록시는 simpara element를 만났을 때 이를 스택에 둔다. 다음에 ulink를 만나면 이것 역시 스택에 둔다. 스택은 이제 simparaulink라는 두 element를 가지고 있는데, 이것은 simpara/ulink 구성이다.

다이어그램의 다양한 단계는 스택의 구성을 나타낸다. 상태 0은 빈 스택을, 상태 1은 한 element (simpara)를 가진 스택을 표시한다. 상태 2는 두 element (simparaulink)을 가진 스택이다.

다시 말해, DFA를 구축하려면 여러분은 가능한 스택 구성만큼 많은 상태를 할당해야 하고 이 상태들간의 변환 기능을 산출해야 한다.

여러분이 상상할 수 있듯이, 그림 1은 너무 간단하다. 실제 프록시는 하나가 아닌 여러 개의 XPaths를 인식하려 할 것이기 때문에 여러 변환 다이어그램을 병행하여 처리할 것이다. 예를 들어, 프록시는 다음 세 XPaths 중 하나를 찾을 것이다.:

simpara/ulink
/
/article/articleinfo/title

DFA가 더 많은 XPaths를 인식하려고 시도하면 상태 번호가 증가할 것이다. 실제로, 스택 측면에서 생각하면 simpara/ulink를 인식하는 것보다 이들 세 XPaths를 인식하기 위해 더 많은 스택 구성이 있을 것이다..

QName

XML elements는 이름 공간 URI와 지역명(local name)이 결합된 것으로 식별된다. 이 결합을 좀 더 효과적으로 처리하기 위해 나는 QName 클래스를 만들었다. XML 노드를 완벽하게 식별하기 위해 QName은 또한 노드의 특성 (element, 속성, 혹은 루트(/))를 기록한다. QName의 코드는 Listing 1에 있다.

QName은 해시 테이블과 호환하기 위해 equals()hashCode()를 구현한다.

HCNode

알고리즘은 XPaths 세트를 처리하고 세 가지를 컴파일한다. 변환 다이어그램이 거칠 상태들, 한 상태에서 다른 상태로 이동하는 변환 기능, 그리고 어떤 상태가 Xpath를 성공적으로 인식했는지를 나타내는 표시가 그것이다.

알고리즘은 프런트 엔드 (프런트 엔드와 백 엔드에 대한 상세 사항은 이전 컬럼 참조)가 인식할 모든 XPaths를 포함한 parse 트리를 반환할 것이라고 기대한다. Listing 2는 트리에 나타난 element들인 HCNode이다. 그러나 이 목록은 HC 개발의 현 단계에서는 불완전하다는 점에 주의한다.; 다음 컬럼에서 좀 더 완성된 버전을 보여주겠다.

HCNode는 컴파일러에 특화되어 있기 때문에 org.ananas.hc.compiler 패키지에 있다. QNameorg.ananas.hc에 있는데, 프록시가 이를 이용할 것으로 기대하기 때문이다.

노드는 (자신의 type 속성에 따라) 다음 중 하나가 될 수 있다.:

  • QName에 의해 표현되는 XML 노드
  • XML 노드들간의 "부모" 관계를 표시하기 위해 XPath에 있는 / 구분자
  • 프록시가 두 개, 혹은 그 이상의 XPaths를 인식했을 때 XPaths들의 결합
  • parse 트리의 끝을 표시하는 특수 노드

XML 노드는 QName에 대한 참조를 가지고 있다. 다른 노드 유형은 트리를 구축하기 위해 왼쪽과 오른쪽 HCNodes에 대한 참조를 가지고 있다.

이전 컬럼에서 설명한대로, 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가 될 것이다. simparark가 XPath simpara에 매치되는 유일한 XML 노드이기 때문이다.

XPath simpara/ulink는 각각 simparaulink를 가리키는 좌우 노드와 함께 PARENT_OF 유형의 n 노드로 parsing될 것이다. XPath의 시작과 매치되는 XML 노드가 simpara이고 XPath의 끝과 매치되는 노드가 ulink이기 때문에, n.firstpos()메소드는 simpara이고 n.lastpost()ulink이다.

표 1firstpos()nullable()을 산출하기 위한 규칙을 설명한다. lastpos()firstpos()과 유사하지만, leftright에 대한 규칙이 반대이다. 여러분은 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 노드 세트이다. 결국 나는 하나의 상태로 변할 유일한 숫자를 이 세트들 각각에 할당할 것이다.

DFA 산출하기

parse 트리가 주어진다면, 여러분은 변환 기능을 처리하기 위해 Listing 3의 알고리즘들을 적용한다. 여러분은 변환 기능을 이차원 행렬인 dtran으로 나타낼 수 있는데, dtran은 주어진 XML 노드에게 다음 단계를 반환한다. Listing 3의 알고리즘은 실제 자바 코드가 아닌 의사 코드임에 유의한다.

Listing 3. 알고리즘의 의사 코드
dstates <- root.firstpos()
for-each s as a state in dstates
   for-each a as an XML node in the input vocabulary
	   U <- s.followpos(a)
		if U is not empty and is not in dstates
		   dstates.append(U)
	   dtran[s,a] <- U

followpos(a)은 XPath에서 어떤 XML 노드가 주어진 XML 노드를 따를 것인지를 말해준다. 여러분은 다음 규칙을 적용해서 이를 산출할 수 있다. :

  • nPARENT_OF 노드이고 a가 left.lastpos() 내의 한 XML 노드일 때, right.firstpos()의 모든 노드들이 followpos(a)내에 있다.
  • n이 상대 경로를 나타내고, a가 n.lastpos()에 있을 때, n.firstpos()의 모든 노드들이 followpos(a)내에 있다

JUnit
나는 이번 컬럼에서 이 알고리즘을 완전하게 구현하고 싶었지만, JUnit를 배우는데 얼마간의 시간을 투자해야 했다. JUnit는 자동화된 테스팅을 위한 오픈 소스로 된 프레임워크이다 (참고 자료). 나는 컴파일러를 작성할 때 자동화된 테스팅이 절대적으로 필요하다는 사실을 어렵게 알아 냈다.

자동화된 테스팅

소프트웨어 프로젝트에서 명백한 하나의 사실은 프로그래머가 자신의 소프트웨어에 버그를 끼워 넣는다는 것이다. 나는 일반적인 프로그래머는 코드 10 라인당 1개의 버그를 작성한다는 통계를 읽은 적이 있다. 테스트는 버그를 발견하고 고치기 위해 설계되었다. 테스팅은 매우 반복적이고 지겨운 작업의 전형적인 예이다. 소프트웨어를 테스트할 때 창조성이란 거의 없다.: 특정 값을 주고 결과를 보기만 하면 된다. 결과가 여러분이 예상한 것과 다르면 여러분은 버그를 발견한 것이다.

아주 지겹고 반복적이기 때문에 테스트들은 (적어도 일부 테스트들은) 자동화되어야 한다. 어찌 되었건, 컴퓨터는 프로그래머보다는 인내심이 강하다. 따라서 컴퓨터는 지겹고 반복적인 작업을 수행할 이상적인 후보자이다.

자동화된 테스팅은 여러분이 다른 소프트웨어를 테스트하기 위한 소프트웨어를 작성함을 의미한다. 이 방식의 장점은 여러분이 필요할 때마다 자주 테스팅 소프트웨어를 실행시킬 수 있다는 점이다. 우리는 애플리케이션에서 변경된 부분만 테스트하는 경우가 너무나 많다. 이 때 문제는 물론 무엇이 변경되었는지를 결정하는 것이다. 코드 내 한 섹션의 변화가 다른 섹션에서 버그를 만들어내는 것은 흔한 일이다. 여러분이 두 섹션이 관련되어 있다는 것을 기억하지 못한다면 여러분은 이 상황에 대해 테스트하지 않을 것이다.

반면 자동화된 테스트는 억지스러운 방식을 택한다. 이들은 실행될 때마다 전 애플리케이션을 테스트할 가능성을 가지고 있고, 이는 코드의 관련되지 않은 섹션에서 버그를 발견할 가능성을 높여준다.

자동화된 테스트의 또 다른 이점은 버그가 애플리케이션에 재등장할 수 있다는 점이다. 여러분이 버그를 고칠 때마다 그 버그를 처리하기 위한 테스트를 작성해 두는 것이 좋다. 프로젝트 기간 동안 그 버그가 다시 등장할 때 여러 번 테스트하는 것이 실패할 가능성이 있다.

extreme programming 운동 (참고 자료)은 자동화된 테스트를 보급시켰다. 고백컨대, 나는 자동화된 테스트를 체계적으로 사용하지 않는다. 내 경험에 따르면 자동화된 테스트는 public 인터페이스가 자주 변경되지 않는 클래스에서 가장 효과적이었다. public 인터페이스가 많이 변경되는 클래스에서는 덜 효과적이었고, 여기에는 대부분의 사용자 인터페이스 코드가 포함된다.

자동화된 테스팅의 연산어는 automated이고, 여러분이 자주 사용하지 않을 무언가를 자동화하는 것은 쓸모없는 일이다. 마찬가지로 여러분이 자주 실행시킬 것이라고 알고 있는 것에 대한 테스트를 작성하는 것이 효과적이다. 자동화된 테스트는 또한 트리 조작과 같은 다소 애매한 코드와 특히 컴파일러에 이상적이다.

JUnit
자동화된 테스팅이 전혀 수고를 필요로 하지 않고 고통도 없다는 바보같은 생각은 하지 말기 바란다. 여러분은 테스트 케이스 작성에 시간을 투자해야 하고, 물론 테스트 애플리케이션을 정기적으로 실행시켜야 한다. 실제로 내가 자동화된 테스트에 그렇게 많은 시간을 쏟아 붓지 않았다면 나는 이번 달에 버그 투성이지만 잘 테스트되지 않은 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에 관해 공부하는데) 투자된 시간은 잘 사용되었다고 확신한다. ; 그 시간은 이번 프로젝트 동안 보답될 것이다. 다음 달에는 컴파일러의 실행 버전을 (최적화되지 않은 버전이긴 하지만) 가질 수 있기를 바라며, 프록시에 대해 작업할 계획이다.

참고 자료

  • "Dragon Book" (Compilers: Principles, Techniques and Tools, A. Aho, R. Sethi과 J. Ullman 공저) : 컴파일러 설계와 구축에 관한 권위 있는 문서
  • JUnit은 자동화된 테스트를 실행시키기 위한 오픈 소스로 된 프레임워크이다.
  • Extreme programming (XP)은 보다 나은 품질의 소프트웨어를 얻기 위한 권고안 세트이다.
  • Pragmatic Programmers은 방법론 분야에 최근 등장한 또 다른 경쟁자이다.

필자소개
Benoit MarchalBenoît Marchal 은 벨기에 Namur에 기반을 둔 컨설턴트겸 저작자이다. 그는 최근 XML by Example, second edition을 출간하였다. 그는 또한 Applied XML SolutionsXML and the Enterprise의 저자이기도 하다. 그의 최근 프로젝트들에 대한 상세 정보를 marchal.com 에서 볼 수 있다.



이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의