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

Diagnosing Java Code : 깊이 우선 Visitor와 broken dispatches
목 차:
Composite 패턴
Visitor 패턴
VIsitor가 적합하지 않을 때
깊이 우선 Visitor 패턴
복잡한 계층
주의사항
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
Broken Dispatch 버그 패턴
튜토리얼: 자바 설계 패턴
US 원문 읽기
Visitor 패턴 변종으로 코드 간결성 향상


Eric E. Allen
박사 과정, 자바 프로그래밍 언어 팀, Rice 대학교
2002년 1월

이번 "Diagnosing Java Code" 연재에서 Eric Allen은 Visitor 패턴의 한 변형인 깊이 우선 visitor (depth-first visitors)를 사용해 여러분 코드의 간결성을 높이는 것이 어떻게 가능한지를 설명한다. 그는 기법을 설명하고 잇점과 주의점을 논의하며 사용시 나타나는 버그 패턴에 대해 독자들에게 경고하고 깊이 우선 visitor의 특정 실례를 보여줄 것이다. 이 글을 읽은 후 여러분은 코딩할 때 도움이 되도록 깊이 우선 visitors를 사용하는 방법을 이해하게 될 것이고 이 기법을 적용할 때의 함정에 대해서도 알게 될 것이다.

설계 패턴은 최적으로 활용할 경우 프로젝트의 간단한 설계를 신속하게 수행하는데 큰 도움이 될 수 있다. 그러나 특정 상황에서 설계 패턴을 구현하는 가장 간단한 방법이 항상 명확한 것은 아니다. 거위를 굽는데는 한가지 이상의 방법이 있다. 이번 달에 우리는 일반적인 디자인 패턴을 적용하여 간단하고 짧고 강력한 코드를 만드는 몇 가지 방법에 대해 알아 보자.

우선 여러 다른 상황에 적용될 수 있는 두개의 패턴을 살펴보자. 본 토픽에 대한 세미자 자료(참고자료)"설계 패턴 (Erich Gamma, "Gang of Four"라고도 알려져 있음)"에서 논의된 모든 설계 패턴 중에서 나는 가장 널리 적용되는 패턴이 Composite와 Visitor 패턴이라는 사실을 발견했다.

이 두 패턴을 좀 더 자세히 살펴보자.

Composite로 반복적인 데이터 유형 지정하기

왜 Composite 패턴이 유용한지는 쉽게 알 수 있다. Composite 패턴은 반복적으로 정의되는 데이터 유형을 지정하는 객체 지향적 방법이다. 이런 기회는 소프트웨어 개발의 모든 과정 중에 등장한다.

반복적으로 정의된 객체 유형에 대해 작업할 수 있는 것은 소프트웨어 엔지니어링 (말하자면, 정형화된 상태의 기계로부터 시스템이 개발되는 디지털 설계와 반대되는 개념)의 가장 두드러진 특징 중 하나이다.

Visitor로 클래스 계층 확장하기

Visitor 패턴이 그렇게 광범위하게 적용되는 이유 중 상당 부분은 Composite 패턴을 보완한다는 사실 때문이다..

Visitors는 기존 Composite 클래스 계층의 기능을 그 계층 내의 클래스를 실제로 수정하지 않고도 확장하는 방법으로 종종 사용된다. 그러나 VIsitors의 기능은 이보다 훨씬 많다.

Visitor들은 클래스 계층의 기능 중 일부를 클래스 자체로부터 분리할 수 있도록 되어 있기 때문에, 클래스의 본질적인 부분이라고 간주하기엔 개념적으로 좀 어색한 기능이 있는 모든 종류의 설정에 사용될 수 있다.

Visitor가 자주 나타나는 흔한 경우가 복합 데이터 구조를 반복적으로 따라 내려갈 때이다. 예를 들어 이진법적 트리 구조를 가진 하나의 클래스 계층이 있고, 하나의 트리에서 어떤 노드가 0(Zero)값을 가지고 있는지를 판별하는 Visitor가 있다고 생각해 보자.

Listing 1. visitor가 들어 있는 이진법적 트리


abstract class Tree {
  public abstract Boolean accept(TreeVisitor that);
}

class Leaf extends Tree {
  public static final Leaf ONLY = new Leaf();

  public Boolean accept(TreeVisitor that) {
    return that.forLeaf(this);
  }
}

class Branch extends Tree {

  public int value;
  public Tree left;
  public Tree right;

  public Branch(int _value, Tree _left, Tree _right) {
    this.value = _value;
    this.left = _left;
    this.right = _right;
  }

  public Boolean accept(TreeVisitor that) {
    return that.forBranch(this);
  }
}

interface TreeVisitor {
  public Boolean forLeaf(Leaf that);
  public Boolean forBranch(Branch that);
}

class ZeroFinder implements TreeVisitor {
  public Boolean forLeaf(Leaf that) {
    return new Boolean(false);
  }

  public Boolean forBranch(Branch that) {
    if (that.value == 0) { return new Boolean(true); }

    boolean foundInLeft = that.left.accept(this).booleanValue();
    boolean foundInRight = that.right.accept(this).booleanValue();

    return new Boolean(foundInLeft || foundInRight);
  }
}

이렇게 0(Zero)값을 찾는 기능을 Tree 클래스로부터 분리하여 for*() 메소드("*"는 "잎"이나 "가지"를 나타내는 와일드카드이다)에 둠으로써 우리는 다음rhk rkxdms 이점을 얻는다.

  • 이 클래스들이 커지는 것을 방지한다. 만약 우리가 새로운 기능이 필요할 때마다 클래스에 새 메소드를 추가한다면 클래스들은 쉽사리 훌쩍 커져버릴 것이다.
  • 0(Zero)을 찾는 기능을 모두 한 곳에 두어 유지보수가 더 쉬워진다.

Visitor가 적절치 않을 때

Gang of Four가 언급했듯이, Visitor를 사용할 때의 주된 단점은 복합 계층에 새로운 클래스들을 추가하기가 조금 더 힘들어진다는 것이다. 이것은 곧 Visitor들은 복합 계층이 변경되지 않을 것으로 예상되는 경우에 사용하는 것이 최선이라는 얘기다. (물론 "확장 Visitor 패턴"과 같이 이것을 돌아가는 방법들이 있기는 하다)

그렇지만 또 다른 큰 단점이 있다. 복합 계층내의 많은 클래스들이 동일한 방식으로 처리되는 경우 각각의 클래스내에 둘 때보다 Visitor내에 둘 때 코드가 더 커질 것이다. (클래스 내에 위치시킬 경우, 간단히 디폴트 구현 내용을 부모 클래스에 둘 수 있기 때문이다)

다행스럽게도 이러한 문제를 해결할 뿐만 아니라 실제로 많은 경우 각각의 클래스에 둘 때보다 코드를 훨씬 더 간단하게 만들 수 있게 하는 일반 Visitors의 변종이 있다. 우리는 이러한 변종을 "깊이 우선 Visitor 패턴"이라고 부른다.

깊이 우선 Visitor 패턴

깊이 우선 Visitor 패턴 뒤에 깔린 생각은 다음과 같다: 데이터 구조를 반복적으로 따라 내려가는 대부분의 Visitor들은 깊이 우선의 방식으로 이를 수행한다. 이것은 Visitor들이 노드 자체를 방문하기 전에 주어진 (잎은 아니고) 노드의 자식 노드를 방문한다는 의미이다.

그러므로 우리는 visitor 추상 클래스의 for* 메소드 내에 깊이 우선의 탐색을 구현할 수 있고, 그리고 나서 각각의 데이터 유형에 대해 하위 노드들의 방문 결과를 결합시키는 방법에 관해 구체적인 구현 상세를 단순히 기술하기만 하면 된다. 이러한 기술 내용은 하위 노드들에 대한 방문 결과를 매개변수로 하는 특수한 for*Only() 메소드 내에 둔다.

예를 들어, Listing 2는 우리의 이진법적 트리들에 대해 깊이 우선의 Visitor를 재작성하는 방법을 보여준다.

Listing 2. 깊이 우선 Visitor


abstract class DepthFirstTreeVisitor implements TreeVisitor {

  public abstract Boolean forLeafOnly(Leaf that);
  public abstract Boolean forBranchOnly(Branch that,
                                        Boolean left_result,
                                        Boolean right_result);

  public Boolean forLeaf(Leaf that) {
    return forLeafOnly(that);
  }

  public Boolean forBranch(Branch that) {
    Boolean left_result = that.left.accept(this);
    Boolean right_result = that.right.accept(this);

    return forBranchOnly(that, left_result, right_result);
  }
}

이제 우리는 Listing 3에 나타난 것과 같이 우리의 ZeroFinder Visitor를 재작성할 수 있다.

Listing 3. 재방문된 ZeroFinder


abstract class DepthFirstTreeVisitor implements TreeVisitor {

class DepthFirstZeroFinder extends DepthFirstTreeVisitor {
  public Boolean forLeafOnly(Leaf that) {
    return new Boolean(false);
  }

  public Boolean forBranchOnly(Branch that, 
		               Boolean left_result, 
                               Boolean right_result) 
  {
    if (that.value == 0) { return new Boolean(true); }
    return new Boolean(left_result.booleanValue() || 
                       right_result.booleanValue());
  }
}

이로써 우리는 구체적인 Visitor로부터 깊이 우선 탐색의 복잡함을 많은 부분 끄집어내어 (공유되는) 추상 부모 클래스로 집어넣었다. 또한 깊이 우선의 Visitor 구현자는 각 유형에 대해 모든 보유 컴포넌트들을 쉽게 사용할 수 있게 되었을 것이다.(for*Only()메소드의 서명에서)

복합 계층은 어떤가?

이것은 이 간단한 복합 계층에 대해 우리가 할 수 있는 것이다. 그러나 더 복잡한 클래스 계층들에 대해서 우리는 더 많은 일을 할 수 있을 것이다.

많은 클래스를 가진 복합 계층에 대해 주어진 Visitor는 종종 많은 경우를 공통된 (혹은 평범한) 방법으로 처리할 것이다. 이것은 Visitor가 한 쌍의 instanceof 확인 작업들을 대신할 수 있을 때 특히 그렇다. 이 경우들에 있어서는 우리는 간단하게 defaultCase()라는 추상 메소드를 호출하는 for*Only() 메소드의 디폴트 구현을 제공할 수 있다.

Listing 4. 추가된 디폴트 예

abstract class DepthFirstTreeVisitor implements TreeVisitor {

  public abstract Boolean defaultCase(Tree that);

  public Boolean forLeafOnly(Leaf that) {
    return defaultCase(that);
  }

  public Boolean forBranchOnly(Branch that,
                               Boolean left_result,
                               Boolean right_result)
  {
    return defaultCase(that);
  }
  ...
}

이제 이와 같이 복잡한 계층에 깊이 우선 Visitor를 구현할 때, 우리는 단지 디폴트로 처리되지 않는 경우들에 대해 코드를 지정하기만 하면 된다.

복합 계층이 하나의 층보다 더 깊을 때 우리는 또한 계층의 각각의 하위 트리에 대해 별개의 디폴트 메소드를 지정하고 싶을지 모른다. 이러한 디폴트 메소드는 부모 클래스의 디폴트 메소드를 단순히 호출하도록 정의될 수 있다. 그러나 그들은 필요한 경우 무시될 수 있다.

일단 이를 수행하면 일반 Visitor의 모든 장점을 유지하면서 복합 클래스 내에 기능을 포함시키는 간편함도 얻을 수 있다.

물론 경고 사항도 있다.

몇가지 경우에만 깊이 우선으로 따라 내려가는 Visitor에게도 우리는 깊이 우선 Vsitor를 사용할 수 있다. 우리는 깊이 우선을 적용하고 싶지 않은 그러한 for*() 메소드(for*Only() 메소드에 대응됨)를 단지 무시하면 된다.

그러나 이 방식에는 하나의 주의사항이 있다. 이 방식에서는 broken dispatches를 만나기가 쉽다. 기억하는가? Broken Dispatch 버그 패턴은 부모 클래스에서의 구현을 무시하기보다는 한 메소드를 우연히 오버로드할 때 나타난다. 깊이 우선 Visitor에서는 특히 나타나기 쉽다.

부모 클래스의 for*()메소드 (for*Only()메소드가 아닌) 중 하나를 무시하려 한다고 가정해 보자. 이 for*()메소드의 서명은 그것이 작동하는 노드는 취하지만, for*Only()메소드는 달리 하위 노드 방문 결과는 취하지 않을 것이다. 우리가 for*()메소드와 for*Only()메소드를 둘다 쓰고 있기 때문에 이를 생략하고 for*()메소드 중 하나의 이름으로 for*Only()를 우연히 쓰는 것을 상상하기란 어렵지 않다.

그렇게 하면 코드는 문제 없이 컴파일되겠지만, 우리는 for*() 기법을 무시하는데 실패하게 될 것이다. 대신 for*Only() 메소드를 다른 서명을 가진 새로운 메소드로 오버로드할 것이다.

추가적으로, 이 새로운 for*Only() 메소드는 결코 호출되지 않을 것이다. 이와 같은 Broken Dispatch는 추적하기가 어려운 매우 잠행성의 버그이다. 타입 검사기로는 이 버그를 잡아 내지 못할 것이고 , 이 버그가 존재한다 해도 여러분은 이들이 언제 실제로 에러를 표시할지 알지 못할 것이다. 최악의 경우 Broken Dispatch는 이치에는 완벽하게 맞지만 완전히 잘못된 방식으로 여러분 프로그램의 행동을 간단히 수정할 것이다.

이를 방지하기 위해 내가 할 수 있는 말은 이 버그가 코드를 작성하기 전에 (그리고 작성하는 동안) 단위 테스트를 작성하는 것이 왜 시간을 줄여주는지, 그리고 이 테스트들을 왜 자주 실행시켜야 하는지를 보여주는 아주 훌륭한 실례라는 것 뿐이다.

이제 여러분은 깊이 우선 visitor가 왜 멋진지, 그리고 그들이 여러분을 어떻게 괴롭힐 수 있는지를 알게 되었다. 나는 이 패턴을 처음 나에게 소개시켜준 우리 실험실의 대학원생인 Brian Stoler에게 감사를표하고 싶다. 깊이 우선 Visitor는 JavaCC 문법으로부터 복합 계층 (그리고 Visitor)을 자동으로 생성시켜주는 "자바 트리 빌더"에 의해 구축되었다. 이 도구는 참고 자료 에서 무료로 이용할 수있다.

참고 자료

  • 표준 Visitors는 최초의 설계 패턴 책인 설계 패턴 : 재사용 가능한 객체 지향 소프트웨어의 요소 (Erich Gamma, Addison-Wesley 1995)에서 논의되었다. 1998년 업데이트본 에는 샘플 코드와 붙여넣기 하여 쓸 수 있는 23개의 패턴이 들어 있다.
  • 설계 패턴이 생소한 분이라면 "자바 설계 패턴" (developerWorks, January 2002)로 시작할 수 있다. 이 책은 중요하고 유용한 객체 지향 설계와 개발 개념을 소개하고 있다.
  • Java Tree Builder 로 깊이 우선 Visitor을 자동으로 생성하라. 이 도구는 Java Compiler Compiler (JavaCC) 파서 생성기와 함께 사용되는 구문 트리 빌더로써, 평이한 JavaCC 문법 파일을 입력문으로 취해 다음과 같은 것을 자동으로 생성한다.문법내의 productions에 기반한 구문 트리 클래스 셋트 (Visitor 설계 패턴을 사용하여); 두개의 인터페이스 (Visitor와 ObjectVisitor); 두 개의 깊이 우선 Visitor (DepthFirstVisitor와 ObjectDepthFirst); 검사하는 동안 구문 트리를 구축하기 위한 적합한 주석을 갖춘 JavaCC 문법
  • Extensible Visitor 패턴에 대한 추가 정보는 "객체 지향 설계와 기능적 설계를 통합하여 재사용성 높이기" (Shriram Krishnamurthi)을 참조한다. 이 두 방식의 장점(기능적 설계의 툴 추가와 객체 지향 프로그래밍의 데이터 셋트 확장)만 통합한 복합 설계 패턴이 제시되어 있다.
  • JUnit 웹 사이트: 프로그램 테스트 기법에 관한 수많은 흥미있는 자료로의 링크 제공
  • Eric Allen의 Diagnosing Java Code 컬럼 전체
  • developerWorks Java technology zone: 자바 관련 자료

필자 소개
Eric Allen은 Cornell 대학에서 컴퓨터 공학 및 수학 학사 학위를 받았으며 Rice 대학의 자바 프로그래밍 언어팀에서 박사 과정을 밟고 있다. 학위를 마치기 위해 Rice 대학으로 돌아가기 전에 Eric은 Cycorp 사의 선임 자바 소프트웨어 개발자였고, 또한 JavaWorld의 자바 초급자 토론 포럼의 진행자이기도 하다. 그는 자바 언어에서 소스와 바이트코드 레벨 양쪽에서의 의미론적 모델 개발과 정적인 분석 툴에 관심을 가지고 있다. Eric은 또한 일반적인 런타임 유형을 가진 자바 언어의 확장판인 NextGen 프로그래밍 언어의 컴파일러 개발을 돕기도 했다.



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

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

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