본문 바로가기

Design Pattern

[디자인패턴] Visitor Pattern (방문자 패턴)

  • 방문자 패턴의 목적과 사용이유
  • 방문자 패턴이란
  • 방문자 패턴 예시 (Car)
  • accept 메소드 중복 구현 피해보기 (시도)
  • Double Dispatch
  • 고려사항
  • Summary

 

  • 방문자 패턴의 목적과 사용이유

Purpose

    - (방문자 패턴이 필요 없는 경우) runtime에 object들에게 operation을 포함시키는게 타당할때

    - (방문자 패턴이 필요한 경우) object 내부에 operation들로 인해 cohesion이 떨어질때

Use When

    - object구조에 object 속성과 관계없는 operation이 많이 수행되어야 할 때

    - object구조는 변하지 않지만 operation들이 많이 변경될 때

    - 같은 인터페이스를 구현하는 다수의 object에 대해 연산을 수행해야 하는 operation이 필요할 때

    - 그 operation들이 abstract class 수준이 아닌 concrete class 수준에서 수행되어야 할 때 (concrete class 마다                    opeartion이여도 다르게 동작해야 할 때)

 

Example

    - 여러 object들에 대해 수행되어야 하는 operation 이 있는데 이게 모든 class들에게 분배돼있으면 이해하기도, 유지보수하기도 힘들다. 이때 만약 새로운 operation이 추가되려 한다면 추가하는데 너무 많은 수정!

Motivations

    - object구조에 많은 class의 object들이 존재한다.

    - 그리고 이 object들에게는 자신들의 속성과 관련없이 수행되어야 하는 operation들이 존재한다.

    - 이때, operation이 기존 object 구조와 연관이 없으므로 class의 cohesion을 보호하고자 한다. (avoid "polluting")

    - object 구조는 거의 바뀌지 않는데 operation들이 빈번히 바뀌어야하는 경우가 생긴다.

    => 따라서 Visitor Pattern이 등장하게 되었다!

Benefits

    - 새로운 operation을 추가하는게 쉬워진다.

    - object 속성과 연관된 operation은 object 내에 위치시키고, 연관되지 않는 operation은 분리할 수 있다.

 

 

 


 

  • 방문자 패턴이란

Visitor Design Pattern

방문자 패턴은 기존의 object structure와 operation을 캡슐화한 Visitor 부분이 나뉘어진 구조이다.

Visitor 패턴내의 메소드는 Concrete element 개수만큼 존재한다. (#Visitor method = #Concrete element)

Concrete Visitor는 각각 Element들을 위한 특정 operation이다.

 

  • Visitor : object구조내의 ConcreteElement 개수만큼 visit operation을 선언한다.
  • (#Visitor method = #Concrete element)
  • ConcreteVisitor : Visitor에서 선언된 operation들을 구현한다.
  • Element : element들이 visitor를 인자로 받기위한 accept 메소드를 정의한다.
  • ConcreteElement : visitor를 인자로 받기위한 accept 메소드를 구현한다.
  • ObjectStructure : elements들을 나열한다. visitor가 element들을 방문하는데에 high-level interface를 제공한다. (ex. Car class)

Collaborations

=> ObjectStructure에서 ConcreteElementA.accept(Visitor)를 호출하면 ConcreteElement가 Visitor.visit(자기자신)을 호출하고 그러면 Visitor가 A에게 맞는 operationA()를 제공한다.

 

 즉, ObjectStructure 에서 Visitor를 accept하면 ConcreteElementA가 그 Visitor를 accept하고, Visitor에게 자기자신을 인자로 넘겨서 visit함수를 호출하게끔 한다. 그러면 Visitor는 ConcreteElementA를 위한 operationA를 제공한다.

다음으로 ConcreteElementB가 그 Visitor를 accept하게 되고 위와 마찬가지로 그러면 Visitor는 ConcreteElementB에게 operationB를 제공한다.

 

 

 

 


 

  • 방문자 패턴 예시 (Car)

Visitor Pattern (ex. Car)

 자동차 파트들이 존재하고 컴포지트(Composite) 패턴을 활용한 Car 클래스가 있다. Car 클래스는 자동차 파트 element들를 추가할 수 있고 Object Structure로써 accept 메소드를 호출하면 모든 element에서 accept 메소드를 호출하게 한 후 자신을 visit하도록 visitor에게 자기자신을 인자로 넘겨준다.

 

Visitor & Element interface

 Visitor interface에서는 Concrete Element 개수만큼 각각에 상응하는 개수의 operation을 정의하고 있다. 

Element interface에서는 Visitor를 인자로 받아 자신을 방문할 수 있게끔 하는 accept 메소드를 정의하고 있다.

CarElementPrintVisitor, CarElementDoVisitor 라는 Concrete Visitor가 존재하며 이는 각각 Concrete Element 개수만큼 상응하는 operation개수를 가지고 있다.

Concrete Element

 각각 Concrete Element에서 accept메소드를 호출하면 Visitor를 인자로 넘겨받게 되고 그 Visitor에게 자기자신을 인자로 넘겨주어 자기자신을 visit할 수 있게끔 한다.

Concrete Element Car (with Composite Pattern)

 Composite Pattern으로 구현된 Car class는 복수개의 Concrete Element들을 갖을 수 있다. 

Car에서 accept메소드를 호출하면 Visitor를 인자로 넘겨받은 후 Car가 포함하고 있는 Concrete Element들이 순차적으로 그 Visitor를 accept하도록 메소드를 호출하도록 한다. 모든 Concrete Element들이 accept메소드를 호출한 후에는 자기자신을 인자로 넘겨주어 자신을 visit하도록 한다.

Concrete Visitor (#메소드 = #Concrete Element)

 위에서 계속 말했듯이 ConcreteVisitor에서는 ConcreteElement 개수만큼의 메소드(operation)을 구현하고 있다.

Object Structure와 Visitor 구조는 위와같고 이제 이들이 어떻게 동작하는지 전체적인 흐름을 보자.

Main (전체적인 흐름)

1. 먼저 Composite Pattern으로 구현된 Car object가 ConcreteVisitor를 인자로 받아 accept메소드를 호출한다.

2. Car object의 accept메소드는 본인이 포함하고 있는 ConcreteElement들이 순차적으로 accept메소드를 호출하게 한다.

3. 특정 ConcreteElement에서 ConcreteVisitor를 인자로 받아 accept 메소드를 이제 호출하고 나면 그 ConcreteVisitor에게 자기자신을 인자로 넘겨주어 visit메소드를 요청한다.

4. ConcreteVisitor에서는 인자로 받은 ConcreteElement에 상응하는 visit메소드(operation)을 호출한다.

 

의문사항 : visit 메소드 중복?!

Do we really need to repeat??

 의문사항 : visitor.visit() 코드를 계속 이렇게 꼭 반복해야만 하는가?

=> 코드의 중복같지만 사실상 각각의 visit(this) 메소드는 인자로 받은 ConcreteElement에 상응하는 visit 메소드를 제공하기 때문에 전부 다른 메소드를 호출하고 있는 것이다.

그렇다면 accept메소드는 중복인가 아닌가?

 

 

 


 

  • accept 메소드 중복구현 피해보기(시도)

Element interface, Concrete Element 수정

 accept메소드의 중복을 피하기 위한 시도는 위와 같다. Car를제외한 Concrete Element의 경우 accept메소드를 호출하면 모두 자기자신을 인자로 visitor에게 던져주는 같은 코드로 메소드를 구현하고 있다. 따라서 accept 메소드의 경우는 상위 인터페이스에서 accept메소드를 구현하게끔 하여 중복을 피할 수 있을 것 같다. 

상위 interface를 abstract class로 바꾸고 accept메소드를 visitor.visit(this)로 구현하면 Concrete Element들은 accept메소드를 따로 구현할 필요가 없으므로 코드의 중복을 줄일 수 있다.

Visitor interface, Concrete visitor 수정

 이때 주의할 점으로는 위와 같이 accept메소드를 상위클래스에 넣음으로써 이제 visitor.visit(this)의 this는 자기자신  abstract class 인 ICarElement 인스턴스를 뜻하게 된다. 따라서 Compile시 Car를 제외한 Concrete Element들의 accept 메소드는 visit(ICarElement)로 인식된다. Compile 오류를 피하기 위해 Visitor interface와 Concrete visitor 에 IcarElement(Element interface)를 인자로 받을 수 있는 visit()메소드를 dummy로 하나 더 구현하였다.

이제 모든 준비가 끝났고 실제로 코드를 돌려보면 우리가 생각한대로 결과가 나올 것 같다.

 

그러나 생각과 다르게 Visitor에서 ConcreteElement를 인자로 받는 visit()메소드를 호출해주는 것이 아닌 Compile을 위해 추가해주었던 dummy결과가 나온다. 왜그런 걸까????

왜 그런지 다음 목차에서 파헤쳐보자.

 

 


 

  • Double Dispatch

위의 문제에 대한 이유에 대해 설명하기 전에 Method Overloading과 Method Overriding에 대해 한 번 다뤄보자.

Method Overloading

Method Overloading의 경우 인자의 개수에 따라 호출되는 method가 달라지므로 compile time에 바인딩 된다.

Method overriding

 반면에 Overriding은 method를 호출하는 object의 type에 의해 결정되므로 dynamic polymorphism이다. run-time에 type이 바뀜에 따라 호출되는 method가 실행중에 달라진다. (즉, 컴파일러가 코드를 실행해보기 전엔 어떤 메소드가 호출될지 모른다.)

Visitor example (static? dynamic?)

 visit(Aluminum), visit(Paper), visit(Glass) 중 어떤게 호출될지는 static polymorphism이다. 인자로 들어오는 object의 type만 보고 결정할 수 있기 때문이다. 그러나 특정 visit(...) 메소드가 PriceVisitor, WeightVisitor 중 누구의 visit() 메소드인지 알 수 없다. a.visit(...)에서 a가 run-time중에 바뀔때마다 호출되는 visit()메소드가 바뀌기 때문에 compile-time에 바인딩 될 수 없다.

 

Double Dispatch is more than Method Overloading

얼핏 보이기로는 double dispatch는 method overloading와 같이 보인다. 왜냐면 method overloading처럼 method가 전달받은 인자에 의해서 결정되기 때문이다. 그러다 Double Dispatch와 다르게 method overloading은 compile time에 결정된다. method overloading에서는 컴파일러가 "name mangling"을 통해서 같은 이름의 method지만 인자의 type에 따라 각기 다른 이름을 부여한다. 

 

 

  • Double Dispatch (Java, C++ 지원 X)

method를 호출할 때 run-time에 의존하는 두개의 object에 의해 method가 결정되는 것을 Double Dispatch라 한다.

예를들어, (a, b).method(...) 와 같은 형태에서 method()를 결정할때 run-time에 결정되는 a와 b에 의해 결정되는 것이다.

 

  • Single Dispatch (Java, C++ 지원 O)

대부분의 object-oriented system에서 concrete method는 dynamic type의 object 하나에 의해 결정된다. 

예를들어, a.method()와 같은 형태에서 method()를 결정할때 run-time에 결정되는 a에 의해 결정되는 것이다.

 

Visitor Pattern and Single Dispatch

Visitor Pattern을 구현하려면 single dispatch를 지원하는 프로그래밍 언어이여야 한다.

element에는 visitor를 인자로 받는 accept 메소드가 존재한다.

accept 메소드는 visitor의 visit메소드를 호출한다. 이때 element는 visit메소드에게 자기자신을 인자로 넘겨준다.

 

Visitor Pattern 에서 Double-Dispatch를 구현하는 방법

위의 accept 메소드가 호출되면 이는 두 가지에 의해 구현메소드가 선택된다.

1. element의 dynamic type (컴파일러가 element는 dynamic type으로 받아들이고 고려함)

2. visitor의 static type (컴파일러가 visitor는 인자로 들어왔으므로 static type으로 받아들임)

그 후, visit 메소드가 호출되면 이는 두 가지에 의해 구현메소드가 선택된다.

1. visitor의 dynamic type (컴파일러가 visitor는 dynamic type으로 받아들임)

2. element의 static type (컴파일러가 element는 인자로 왔으므로 static type으로 받아들임)

    - 이때, element는 accept 호출에서 dynamic type이다.

* visit 메소드가 인자로 (this) (즉, interface형) 전달하므로 visitor에 이 element interface형을 인자로 받는 visit메소드가 정의되어있지 않으면 컴파일러가 에러를 일으킨다.

 

 

결국 visit 메소드는 두 가지에 의해 선택된다.

1. element의 dynamic type

2. visitor의 dynamic type

 

 

 


 

  • 고려사항
  • object structure을 순회하는 책임을 누가 갖고있는 것이 맞는가??

Iterator가 맡는게 제일 깔끔하고 적절하다. > Object Structure(ex. Car class) > Visitor(불가피한 경우)

Visitor가 operation관리 뿐만아니라 element들의 순회까지 맡는건 상당히 안좋다. 그러나 불가피하게 visitor가 operation을 수행한 다음에서야 다음 갈 visitor를 알 수 있는 특수한 경우에 visitor가 traversal의 책임까지 맡게된다.

 

  • Benefits

Visitor에 새로운 operation을 추가하는것이 쉬워진다. (Concrete Visitor 추가하고 여기만 코드 수정하면 끝)

Visitor는 연관된 operation들을 모으고 연관없는 operation들은 분리한다.

    - 연관된 behavior들은 ConcreteVisitor에 한데모으고

    - 연관없는 behavior 집합들은 다른 ConcreteVisitor로 분리시킨다.

 

  • Liabilities (단점)

    - 새로운 ConcreteElement를 추가하는 것은 힘들다. ConcreteElement가 추가되면 Visitor interface 뿐만 아니라 모든 ConcreteVisitor에 코들르 추가해야한다.

    - Accumulate state : Visitor를 통해서 모든 element들을 순회하며 element 상태를 확인할 수 있다. 따라서 프로그래머 입장에서는 편하지만 설계입장에서는 메소드들이 element 상태에 따라 메소드 실행결과가 매번 달라져서 단점이다.

    - Breaking encapsulation : visitor가 element의 정보에 접근해야하는데 visitor의 기능이 많아질수록 element입장에서는 점점 더 많은 정보들을 public으로 제공하여 visitor가 접근할 수 있게 해줘야 한다. -> 인캡슐이 줄어들음

 

 

 


 

  • Summary

장점 : 새로운 operation을 추가하는게 쉽다. 연관된 operation들을 모으고 연관되지 않는 operation들을 분리할 수 있다.

단점 : 새로운 ConcreteElement를 추가하는게 어렵다. encapsulation을 무너뜨릴 수 있다.