본문 바로가기
SW

GoF의 디자인 패턴(Design Patterns: Elements of Reusable Object-Oriented Software) - 5장 행동패턴 :: 방문자(Visitor)

by 라꾸스떼(YR) 2020. 5. 14.
반응형

[방문자(Visitor) - 객체 행동]

<의도>

객체 구조를 이루는 원소에 대해 수행할 연산을 표현한다. 연산을 적용할 원소의 클래스를 변경하지 않고도 새로운 연산을 정의할 수 있게 한다.

<동기>

연산들이 여러 노드 클래스에 걸쳐 분산되어 있어 시스템의 이해 및 유지보수, 변경 작업이 어렵다는 점이다. 타입 점검 코드가 장식 인쇄 코드 또는 흐름 분석 코드와 섞이면 혼란스럽다. 게다가, 연산 하나를 새로 추가하려면 관련된 모든 클래스를 재컴파일해야 할 때도 태반이다.

문제를 해결하는 방법으로 각 (노드) 클래스에서 서로 관련된 연산들을 추려 모아 별도로 하나의 객체로 묶는다. 이런 객체를 가리켜 방문자라고 한다. 그리고 이 방문자 객체를 추상 구문 트리의 원소에 전달하여 순회시키는 것이다. 트리의 원소가 방문자를 “수락(accept)”하면, 그것의 클래스를 인코딩하는 방문자에게 요청을 보낸다.

방문자 패턴을 사용하면 두 개의 클래스 계통이 정의된다. 하나는 연산이 적용되는 원소에 대한 클래스 계통(Node 클래스 계통)이고, 또 하나는 그 원소에 대해 적용할 연산을 정의하는 방문자 클래스 계통(NodeVisitor 클래스 계통)이다.

<활용성>

-다른 인터페이스를 가진 클래스가 객체 구조에 포함되어 있으며, 구체 클래스에 따라 달라진 연산을 이들 클래스의 객체에 대해 수행하고자 할 때

-관련되지 않은 ㅁ낳은 연산이 한 객체 구조에 속해있는 객체들에 대해 수행될 필요가 있으며, 연산으로 클래스들을 “더럽히고” 싶지 않을 때.

-객체 구조를 정의한 클래스는 거의 변하지 않지만, 전체 구조에 걸쳐 새로운 연산을 추가하고 싶을 때. 객체 구조가 자주 변경될 때는 해당 연산을 클래스에 정의하는 편이 더 낫다.

<참여자>

-Visitor:객체 구조 내에 있는 각 ConcreteElement 클래스를 위한 Visit() 연산을 선언한다. 연산의 이름과 인터페이스 형태는 Visit() 요청을 방문자에게 보내는 클래스를 식별한다.

-ConcreteVisitor: Visitor 클래스에 선언된 연산을 구현한다. 각 연산은 구조 내에 있는 객체의 대응 클래스에 정의된 일부 알고리즘을 구현한다. ConcreteVisitor 클래스는 알고리즘이 운영될 수 있는 상황 정보를 제공하며 자체 상태를 저장한다. 이 상태는 객체 구조를 순회하는 도중 순회 결과를 누적할 때가 많다.

-Element: 방문자를 인자로 받아들이는 Accept() 연산을 정의한다.

-ConcreteElement: 인자로 방문자 객체를 받아들이는 Accept() 연산을 구현한다.

-ObjectStructure: 객체 구조 내의 원소들을 나열할 수 있다. Object Structure는 Composite 패턴으로 만든 복합체일 수도 있고, 리스트나 집합 등 컬렉션일 수도 있다.

<협력 방법>

-방문자 패턴을 사용하는 사용자는 ConcreteVisitor 클래스의 객체를 생성하고 객체 구조를 따라서 각 원소를 방문하며 순회해야 한다.

-방문자가 구성 원소들을 방문할 때, 구성 원소는 해당 클래스의 Visitor 연산을 호출한다. 이 원소들은 자신을 Visitor 연산에 필요한 인자로 제공하여 필요하면 방문자 자신의 상태에 접근할 수 있도록 한다.

<결과>

1.Visitor 클래스는 새로운 연산을 쉽게 추가한다.

2.방문자를 통해 관련된 연산들을 한 군데로 모으고 관련되지 않은 연산을 떼어낼 수 있다.

3.새로운 ConcreteElement 클래스를 추가하기가 어렵다. 방문자 패턴을 사용하면 Element 클래스에 대한 새로운 서브클래스를 추가하기가 어려워진다. ConcreteElement 클래스가 새로 생길 때마다, Visitor 클래스에 대한 새로운 추상 연산 및 모든 ConcreteVistor 클래스에 그 연산에 대응하는 구현을 제공해야 한다. 따라서 방문자 패턴을 적용할 때 중요하게 고려해야 할 점은 객체의 구조에 적용될 알고리즘의 변화가 자주 발생하는가, 아니면 이 같은 구조를 구성하는 객체의 클래스에 변화가 자주 발생하는 가이다. Visitor 클래스 계통은 새로운 ConcreteElement 클래스가 자주 추가되는 상황에서는 유지하기가 상당히 까다롭다.

4.클래스 계층 구조에 걸쳐서 방문한다. 방문자는 동일한 부모 클래스가 없는 객체들도 방문할 수 있다. Visitor 인터페이스에 어떤 객체의 타입이라도 추가할 수 있다.

5.상태를 누적할 수 있다.

6.데이터 은닉을 깰 수 있다. 이 패턴을 쓰면 개발자는 원소의 내부 상태에 접근하는 데 필요한 연산들을 모두 공개 인터페이스로 만들 수밖에 없는데, 이는 캡슐화 전략을 위배하는 것이다.

<구현>

각 개체 구조는 자신과 연관된 Visitor 클래스를 가진다. 이 추상 Visitor 클래스는 객체 구조를 정의하는 각각의 ConcreteElement의 클래스를 위한 VisitConcreteElement()연산을 선언한다.

Visitor의 각 Visit 연산의 인자로 ConcreteElement를 정의하여 Visitor는 ConcreteElement의 인터페이스에 직접 접근하게 한다.

ConcreteVisitor 클래스는 각 Visit 연산을 재정의하여 그에 대응되는 ConcreteElement 클래스를 위한 방문자별 행동을 구현한다.

각 ConcreteElement 클래스는 ConcreteElement에 대한 Visitor에 대응한 Visit...연산을 호출하는 Accept()연산을 구현한다.

방문자 패턴을 적용할 때 생기는 구현 이슈 2개.

1.이중 디스패치. 방문자 패턴은 사용자가 클래스를 변경하지 않고 연산을 클래스에 추가하도록 만드는 패턴이다. 이를 위해 이중 디스패치(double dispatch)라고 하는 아주 잘 알려진 기법을 사용한다. (C++는 단일 디스패치)

단일 디스패치 언어에서 어떤 연산이 요청을 처리할 것인지 결정하는 기준은 두 가지이다. 하나는 요청의 이름이고, 또 하나는 수신자의 타입이다.

“이중 디스패치”는 간단히 말해, 실행되는 연산이 요청의 종류와 두 수신자의 타입에 따라 달라진다는 뜻이다. Accept()는 이중 디스패치 연산이다. 즉, Visitor의 타입과 Element의 타입에 따라 다르다. 이중 디스패치를 쓰면 방문자는 원소의 각 클래스에 대해 서로 다른 연산을 요청할 수 있다. 이것이 방문자 패턴의 키포인트이다. 실제로 실행되는 연산은 Visitor의 타입과 그것이 방문하는 Element의 타입에 따라 달라진다는 점이다.

2.누가 객체 구조를 순회할 책임을 지는가? 문제는 어떻게 방문자가 그곳에 가느냐이다. 순회를 위한 임무를 세 곳 중 어떤 곳에라도 놓을 수 있다. 한 군데는 객체 구조이고, 또 한 군데는 방문자이며, 마지막 한 군데는 별도의 반복자 객체이다.

<예제 코드>

-

 

<잘 알려진 사용예>

-

 

<관련 패턴>

복합체 패턴이 정의하는 복합 객체 구조에 대해 연산을 적용하는 데에 방문자를 쓸 수 있다. 방문자 패턴은 해석자 패턴의 해석 과정에도 사용할 수 있다.

반응형

댓글