이미 잘 구현된 클래스 구조에 공통적인 기능을 추가하려면 어떻게 해야할까?
모든 클래스들에 완벽하게 공통적인 기능이라면 최상위 클래스나 인터페이스에 기본동작을 정의하면 될 것이다
하지만, 세부동작이 상이한 경우에는 하위클래스 각각에 재정의 등을 통해서 메서드를 작성해야 한다.
이는 단기적으로 문제를 해결할 수 있으나, 수정과정에서 예상하지 못한 오류가 발생할 가능성이 높다.
또한, 이러한 유형의 기능이 추가될 때마다 코드가 추가되므로 코드복잡도가 상승하고 SRP를 지키기 힘들어질 것이다.
따라서, 기존의 클래스구조는 최대한 건들지않고 외부에 코드를 작성하는게 좀 더 합리적인 방법이다.
이런 상황에서 도움이 될 수 있는 디자인패턴이 Visitor 패턴이다.
기본적인 아이디어는 외부 클래스에서 메서드 매개변수로 객체를 입력받고, 동작을 수행하는 것이다.
이 과정에서 객체별로 세부동작이 다를 수 있으므로 각 객체에 해당하는 메서드를 구축할 수 있다.
고려해야할 사항은 외부에서 Visitor 클래스의 메서드를 호출할 때, 어떻게 선택하느냐
다.
Visitor 클래스 호출에 입력으로 사용되는 객체집합이 하나의 공통 인터페이스로 관리되는 상황일 때,
단순하게 생각하면 if
와 instanceof
를 이용해서 모든 종류의 하위클래스타입을 검사하는 방법이 있다.
for(Node node : graph){
if (node instanceof City)
exportVisitor.doForCity((City) node)
if (node instanceof Industry)
exportVisitor.doForIndustry((Industry) node)
// …
}
이는 문제해결에 직관적인 방법이지만, 코드가 깔끔하지 못하다는 단점이 있다.
이 문제를 해결하기 위해서는 주어진 상황을 점검해볼 필요가 있다.
현재 주어진 객체의 타입은 모두 Node
이며, if
와 instanceof
를 사용하고 싶지 않다.
따라서, Visitor
의 입장에서 주어진 객체에게 어떤 메서드를 호출해야할지 선별하는게 불가능한 상황이다.
그렇다면 반대로 객체의 입장에서 자신을 처리해줄 Visitor
를 선택한다는 구조로 생각해보자
즉, 객체들의 공통메서드로 외부에서 Visitor
를 입력받고, 메서드 내에서 Visitor
의 메서드를 호출하는 것이다.
// Client code
for(Node node : graph){
node.accept(exportVisitor)
}
// City
class City{
void accept(Visitor v){
v.visit(this);
}
// …
}
이 방법을 더블 디스패치라고 부르며, Visitor 패턴에서 주로 사용되는 기법이다.
클래스 구조도를 살펴보면 크게 Visitor
와 Element
로 구분된다.
Visitor
는 추가해야할 작업이 작성되는 외부 클래스이며, Element
는 기존 클래스들을 의미한다.
앞서 설명했던 것처럼 Element
에선 accept()
를 통해 Visitor
를 매개변수로 입력받고, 해당 메서드 내에서 Visitor
의 메서드를 호출한다.
각 클래스 내의 메서드에서 Visitor
의 메서드를 호출하므로 this
를 통해 어떤 클래스타입의 객체가 전달되는지 정확하게 구분이 가능하다. 따라서, Visitor
클래스에서 오버로딩을 통해 구현된 메서드들을 손쉽게 호출할 수 있다.