비지터 (Visitor) 패턴 :: 2007/09/03 18:15
|
|
하지만 비지터 패턴은 프로그래머들로 하여금, 문제를 조금 다른 방식으로 바라볼 것을 주문합니다. 이런 상황을 한 번 생각해 보죠.
객체의 구조는 잘 알려져 있고, 쉽게 변화하지 않는다. 하지만 객체에 적용될 오퍼레이션(메소드)이 어떤 것이 있을지는 지금 잘 모르겠다. 프로그래밍을 해 나가는 와중에 요구사항이 변화하여 새로운 오퍼레이션이 구현되어야 하는 상황이 빈번하게 벌어질 지도 모른다.
이런 상황에 적합한 디자인 패턴이 바로 비지터 패턴입니다. 그래서, 비지터 패턴은 다소 비-객체지향적인 방식의 프로그래밍을 요구하게 될 때도 있습니다. 그 점에 대해서는 나중에 예제를 통해서 살펴보도록 하구요. 우선은 비지터 패턴의 UML 다이어그램부터 잠시 참고하시도록 할까요? 이 패턴의 UML 다이어그램은 은 아주 훌륭한 블로그 중 하나인 http://younghoe.info/165 에 가 보시면 나와 있으니까, 참고하시면 되겠습니다.
자. 그러면 다시 문제로 돌아가 봅시다. 문제는, "객체의 구조는 고정적인데 그 객체에 적용되어야 하는 오퍼레이션은 정해져 있지 않으므로, 그 객체에 둘 수 없다"는 것입니다. 이 대목이 바로 비-객체지향적인 부분이죠. 그렇다면, 그 객체(Element라고 합시다)에 적용되어야 하는 오퍼레이션은 어디에 두어야 할까요? OO에서는 메소드가 오퍼레이션이고, 메소드는 객체 안에 두어야 하니까, 결국 다른 객체에 정의하여야 할 것입니다. 편의상, 이 '다른 객체'를 Visitor, 즉 Element에 대한 '방문자'라고 지칭합시다.
그렇다면, Element에 적용되어야 하는 연산이 Visitor 안에 정의되어 있을 테니까, Element로 하여금 어떤 연산을 하도록 만들려면, Element에게 Visitor를 넘겨주어야 하겠군요. 그렇다면 아마 Element는 다음과 같은 인터페이스를 만족하여야 할 것입니다.
interface Element {
public void accept(Visitor vObj);
}
그렇다면, Element 객체는 누군가 accept 메소드를 호출해서 Visitor 객체를 넘겨주면, 그 객체에 정의된 어떤 함수를 호출해서, Visitor 객체가 Element 객체 대신 모종의 연산을 하도록 만들면 되겠군요. 그렇다면 모르긴 몰라도, Element를 implement하는 클래스의 accept 메소드 안에는, 다음과 같은 코드가 들어있지 않을까요?
class ConcreteElementA implements Element {
public void accept(Visitor vObj) {
vObj.visit(this);
}
}
자. 그렇다면 만약에 Visitor가 인터페이스라고 가정한다면 (당연히 그래야할 것 같지만) 그 인터페이스 안에는 다음과 같은 메소드가 정의되어 있어야 하겠군요.
interface Visitor {
public void visit(Element eObj);
}
그런데 정말 이렇게 해도 되나요? Visitor 인터페이스가 위와 같이 정의되어 있는 경우, 이 인터페이스를 implement해서 Element에 저장된 int 값을 hexadecimal 형태로 찍어주는 메소드를 추가하려면 아마 다음과 같이 해야 할 겁니다.
class HexReportVisitor implements Visitor {
public void visit(Element eObj) {
int i = eObj.getIntValue();
... // 이하 생략
}
}
그런데 이런 식으로 구현을 하게 되면, 결국 인터페이스 Element에 굉장히 많은 메소드(위의 코드에서 사용된 getIntValue()와 같은)가 정의되어야 합니다. 문제는 그 메소드들이 '인터페이스'와는 별 관련이 없는, 객체에 저장되는 값을 추출하는 목적으로 정의되는 메소드들(소위, 'accessor' 메소드들)이라는 것이고, 그런 메소드들을 Element에 추가하게 되면 결국 ConcreteElementA 와 같은 클래스들의 구현이 그런 메소드들에 의해서 제약을 받게 되죠.
그런 구현을 선호하시는 분들도 있을 수 있고, 아닌 분들도 있을 수 있습니다. 하지만 인터페이스의 본래 목적에 비추어 보면, 그런 구현을 좋아하지 않는 분들이 더 많을 것 같아 보여요. 그러니, Visitor 인터페이스는 아마 다음과 같이 정의되는 편이 더 나을 겁니다.
interface Visitor {
public void visit(ConcreteElementA aObj);
public void visit(ConcreteElementB bObj);
...
}
이런 식으로 구현하게 되면, 결국 앞서의 HexReportVisitor 클래스는 다음과 같이 구현되어야 마땅하겠죠.
class HexReportVisitor implements Visitor {
public void visit(ConcreteElementA aObj) {
int i = aObj.getIntValue();
... // 이하 생략
}
public void visit(ConcreteElementB bObj) {
String s = bObj.getValue();
... // 이하 생략
}
}
이렇게 하면 각각의 visit 메소드가 인자로 전달되는 객체의 타입을 구별하기 때문에 그 객체의 타입에 맞는 작업을 정의할 수가 있게 됩니다. ConcreteElementA 객체가 'visit' 될 때 실행되는 visit함수와 ConcreteElementB 객체가 'visit'될 때 실행되는 visit 함수가 달라지는 거지요. 그러니 당연히 그 구현도 다르게 가져갈 수 있습니다.
따라서 굳이 Element 인터페이스 안에 getIntValue() 같은 메소드 선언부를 다 때려 넣을 필요가 없게 됩니다. 그러니 ConcreteElementA, ConcreteElementB, ... 등등의 클래스 간에 심각한 연관성이 없어도 되지요.
물론 Visitor라는 인터페이스를 정의하고 사용해야 하는 관점에서 보면, visitor 인터페이스 안에 저렇게 인자의 타입에 따라 달라지는 수많은 visit 메소드를 정의해야 하는 것이 불만족스러울 수도 있어요. 하지만 지금은 Element의 하위 타입들이 비교적 고정되어 있는 상황을 가정하고 있는데다, 이렇게 하지 않으면 instanceOf 메소드를 남발하는 코드를 작성하게 되는 비극적인 상황에 처하게 될 위험성도 있어서, 이정도로 타협하고 넘어가는 것이 좋아보이기도 합니다.
자. 아무튼 이제 이정도면 앞서 보았던 UML 다이어그램의 의미는 그럭저럭 납득이 되셨으리라 생각됩니다. 위에 보았던 UML 다이어그램과 완벽하게 일치하는 예제를 보여드린 것은 아닙니다만, 이정도로도 Visitor 패턴이 의미하는 바에 대해서는 설명이 되었을 것 같습니다.
어쨌거나 이런 패턴에 따라서 코딩을 하게 되면, 다음과 같은 프로그래밍이 가능하죠.
List<Element> container = new List<Element>();
container.add( new ConcreteElementA( ... ) );
container.add( new ConcreteElementB( ... ) );
...
HexReportVisitor hexReporter = new HexReportVisitor();
TextReportVisitor textReporter = new TextReportVisitor();
for( Element e: container )
e.accept( hexReporter );
// or
for( Element e: container )
e.accept( textReporter );
즉, 필요에 따라서 container 안에 저장된 객체에 대해서 아무 visitor나 호출할 수 있습니다. 그러면 각 객체에 대해서, 지정된 visitor 안에 정의된 메소드가 호출되겠죠. 다른 메소드를 적용하고 싶으면, 다른 visitor를 호출하면 됩니다. 위에서는 hexReporter와 textReporter의 두 가지 visitor를 정의한 다음에, 상황에 따라서 그 중 하나를 호출했습니다.
이런 식으로 코딩할 수 있다는 것은, 결국 객체와 그 객체에 적용될 수 있는 연산을 분리시키는 효과를 낳습니다. 분리된 연산 하나 하나는 별도의 Visitor 클래스에 정의됩니다. 따라서, 프로그래머는 필요에 따라 새로운 Visitor 클래스를 생성하여 새로운 연산을 추가할 수 있습니다.
하지만 보시다시피, Visitor 클래스는 Element 인터페이스를 implement 하는 concrete 클래스들의 implementation에 굉장히 종속적으로 작성되는 경향을 가지게 됩니다. Element 인터페이스를 implement하는 클래스들은 Visitor 클래스들의 구현을 위해 자신의 정보를 accessor 함수들을 통해 남김없이 전달할 수 있어야 하고, 결과적으로 Visitor 클래스들의 구현은 그런 정보들에 종속적으로 작성될 수 밖에 없는 것이죠.
하지만 어차피 객체의 'representation'과 'operation'을 Element-Visitor로 떼 낸 만큼, 그런 종속성 쯤이야 적당히 감수를 해야겠죠.
아무튼, 저는 프로그래밍하면서 이런 패턴을 적용해야 할 정도로 요구사항이 심각하게 변화하는 상황에 처한 적이 없어서 나름 다행이라고 생각합니다만 ㅎㅎ 앞으로는 어떨지 모르겠어요. 뭐... 어떤 알바를 하느냐에 따라서 어떤 프로그래밍을 하게 되느냐 하는 것은 꽤 심각할 정도로 달라지게 되니까 말이죠.
아무튼, 이 정도로 Visitor 패턴에 대한 글을 마무리 짓겠습니다. 나중에 시간나면 세미나용 자료도 작성해서 올려보도록 하겠습니다.