본문 바로가기

취업과 기본기 튼튼/빽 투더 기본기

[디자인 패턴 15편] 행동 패턴, 방문자 (Visitor)

1. 개념

비지터 패턴은 방문자와 방문 공간을 분리하여,
방문 공간이 방문자를 맞이할 때, 이후에 대한 행동을 방문자에게 위임하는 패턴이다.

간단하게 설명하기 참 어려운 패턴이다.
보통 OOP에서, 객체는 그 객체가 하는 행동을 메쏘드로 가지고 있다.
그리고 행동의 대상이 되는 객체가 있을 경우, 메쏘드의 파라미터로 입력받는다.
그런데, 비지터 패턴은 행동의 대상이 되는 객체가 행동을 일으키는 객체를 입력으로 받는다.
설명이 좀 어려운데, 간단히 예로 설명하면 다음과 같다.

"나는 상점에 방문한다. 나는 ~를 한다."
'나'라는 객체가 '상점'이라는 객체를 입력받은 후, 이 상점에 대해서 뭔가를 한다.
이게 일반적인 OOP 추상화다.
반면 비지터 패턴은 다음과 같다.

"상점에 내가 방문을 했다. 내가 ~를 하게 한다."
'상점' 이라는 객체가 '나'라는 객체를 입력받은 후, '나' 라는 객체의 행동을 호출하는 것이다.
이 때, 상점에 대한 정보를 파라미터로 넘겨준다.
즉, 사용자는 방문자의 입장이 아니라, 방문 공간의 입장에서 먼저 생각해보게 된다.

1.1. 구조

출처 : https://www.codeproject.com/Articles/186185/Visitor-Design-Pattern-2

  • Visitor
    • 방문자 클래스의 인터페이스
    • visit(Element) 을 공용 인터페이스로 쓴다. Element 는 방문 공간이다.
  • Element
    • 방문 공간 클래스의 인터페이스
    • accept(Vistor) 를 공용 인터페이스로 쓴다. Visitor 는 방문자다.
      • 내부적으로 Vistor.visit(this) 를 호출한다.
  • ConcreteVisitor
    • Visitor 를 구체적으로 구현한 클래스
  • ConcreteElement
    • Element 를 구체적으로 구현한 클래스

1.2. 장점

  • 작업 대상(방문 공간) 과 작업 항목(방문 공간을 가지고 하는 일)을 분리시킨다.
    • 작업 대상(방문 공간) 은 단지 데이터를 담고있는 자료구조로 만들고,
    • 작업 주체(방문자) 는 visit() 안에 이 작업 대상을 입력받아 작업 항목을 처리하면 된다.
    • 즉, 데이터와 알고리즘이 분리되어, 데이터의 독립성을 높여준다.
  • 작업 대상의 입장에서는 accept() 로 인터페이스를 통일시켜, 사용자에게 동일한 인터페이스를 제공한다.

1.3. 단점

  • 새로운 작업 대상(방문 공간)이 추가될 때마다 작업 주체(방문자) 도 이에 대한 로직을 추가해야 한다.
  • 두 객체 (방문자와 방문 공간)의 결합도가 높아진다.
    • 서로 visit()accept() 에 의존하므로.

1.4. 활용 상황

  • 자료 구조(데이터)와 자료 구조를 처리하는 로직(알고리즘)을 분리해야할 경우
  • 데이터 구조보다 알고리즘이 더 자주 바뀌는 경우
    • 즉, 방문공간은 어느정도 정해져있고 방문자가 더 자주 바뀌는 경우

2. 구현

클라이언트는 다음과 같이 사용할 수 있다.

concrete_visitor_1 = ConcreteVisitor1()

for concrete_element in [ConcreteElementA(), ConcreteElementB()]:
    concrete_element.accept(concrete_visitor_1)

방문자 개념인 Visitor의 추상클래스와 구체적인 클래스는 다음과 같다.

class Visitor(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def visit_concrete_element_a(self, concrete_element_a: ConcreteElementA):
        # concrete_element_a 를 방문했을 때, 처리할 로직
        # 하위 클래스에서 구현한다.

    @abc.abstractmethod
    def visit_concrete_element_b(self, concrete_element_b: ConcreteElementB):
        # concrete_element_b 를 방문했을 때, 처리할 로직
        # 하위 클래스에서 구현한다.


class ConcreteVisitor1(Visitor):
    def visit_concrete_element_a(self, concrete_element_a: ConcreteElementA):
        pass

    def visit_concrete_element_b(self, concrete_element_b: ConcreteElementB):
        pass


class ConcreteVisitor2(Visitor):
    def visit_concrete_element_a(self, concrete_element_a: ConcreteElementA):
        pass

    def visit_concrete_element_b(self, concrete_element_b: ConcreteElementB):
        pass

방문 공간의 개념인 Component의 추상클래스와 구체적인 클래스는 다음과 같다.

class Element(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def accept(self, visitor: Visitor):
        pass

class ConcreteElementA(Element):
    def accept(self, visitor: Visitor):
        visitor.visit_concrete_element_a(self)


class ConcreteElementB(Element):
    def accept(self, visitor: Visitor):
        visitor.visit_concrete_element_b(self)

3. 정리

구현만 보면 별로 어렵지는 않은데, 정확히 어떤 경우에 사용하는지가 좀 의문이긴 하다.
결합도가 높아진다는 측면에서, 그렇게 매우 좋아보이지는 않는데...
찾아보고 찾아봐도, 뭔가 뚜렷하게 사용해야겠다는 느낌이 잘 안온다.

첫 번째 참고링크를 보면, 처음에 다음과 같은 말이 등장한다.

객체에 대한 행위의 내용을 외부 클래스로 빼서 객체의 행위를 위임하기도 한다. 이런 타입의 패턴으로 전략패턴, 커맨드 패턴, 비지터 패턴등이 있다. 셋 모두 객체의 행위를 바깥으로 위임하는 것이지만, 전략패턴이 하나의 객체에 대해 여러 동작을 하게 하거나(1:N), 커맨드 패턴이 하나의 객체에 대하 하나의 동작(+보조동작)에 대한 설계방식(1:1)인 반면에, 방문자 패턴은 여러 객체들에 대해 객체의 동작들을 지정하는 방식(N:N) 이다.

[출처] 방문자 패턴(Visitor Pattern) - 자바 디자인 패턴과 JDK예제

저 3가지를 모두 동시에 봐야 제대로 이해가 될 듯한데... 추후 다시 정리하면서 비교해봐야 겠다.

4. 참고