Notice
Recent Posts
Recent Comments
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

DeFacto-Standard IT

Visitor Pattern 본문

Design Pattern/Behavioral Pattern

Visitor Pattern

defacto standard 2018. 2. 16. 23:51

1. 가정


 - 비디오를 보여주는 프로그램을 만든다.

 - Mpeg, Avi 포맷을 지원한다.


2. Naive Code


 - VideoStrategy

public interface VideoStrategy {
public void doVideo();
}


 - MpegVideoStrategy

public class MpegVideoStrategy implements VideoStrategy {
@Override
public void doVideo() {
System.out.println(this.getClass().getSimpleName() + "# doVideo()");
}
}


 - AviVideoStrategy

public class AviVideoStrategy implements VideoStrategy {
@Override
public void doVideo() {
System.out.println(this.getClass().getSimpleName() + "# doVideo()");
}
}


 - NaiveClient

public class NaiveClient {
public static void main(String args[]) {
VideoStrategy videoStrategy;
videoStrategy = new MpegVideoStrategy();
videoStrategy.doVideo();

videoStrategy = new AviVideoStrategy();
videoStrategy.doVideo();

}
}


실행결과는 다음과 같다.



3. 문제점


위 코드는 GoF Design Patterns 중 Strategy Pattern을 사용하여 구현한 것이다.


추상적인 VideoStrategy를 두어 MpegStrategy나 AviStrategy등 구체적인 요소에 대해 일괄적으로 연산할 수 있게 하였다.


따라서 VideoStrategy를 사용하는 입장에서는 구체적인 Strategy들의 정보를 알 수 없더라도 정상적인 연산이 가능하다.


그러나 다음과 같은 요구사항의 변경이 발생했다고 가정하자.


 - 비디오 매체 뿐만 아니라 오디오 매체까지 보여줄 수 있다.

 - 새로운 포맷도 보여줄 수 있다.

 - 모든 매체는 모든 포맷을 지원한다.


첫 번째 요구사항을 충족시키기 위해,

Video와 동일하게 AudioStrategy를 추가하고, 아래에 MpegAudioStrategy, AviAudioStrategy를 둔다면, 일괄적인 Audio처리가 가능할 것이다.


두 번째와 세 번째 요구사항을 충족시키기 위해, 포맷을 FormatA 라고 한다면,

각 Strategy 밑에 FormatAVideoStrategy, FormatAAudioStrategy 등을 추가해야 할 것이다.


그러나 이는 매체가 증가될 때마다 클래스의 추가 및 수정이 심각해진다.


만약, 매체가 계속해서 늘어난다고 가정해보자. MediaA/B/C/D/EStrategy 밑에 FormatA/Mpeg/Avi 를 지원하는 클래스를 모두 만들어야 한다.


매체에 관련된 Strategy가 너무 많아졌기 때문에, 이를 추상화한다고 생각해보자.

MediaStrategy라는 개념을 추상적 개념으로 놓을 목적이라면, MediaA/B/C/D/EStrategy가 모두 interface에서 class로 변경되어야 한다.


그리고, 포맷Strategy는 모두 implements 키워드가 아니라 extends 키워드로 변경되어야 한다.



이렇게 전부 변경했다고 쳐도, 클래스 숫자를 계산해보자.

우선 매체를 일괄적으로 다루는 최상위Strategy 1개

매체 A B C D E Strategy 5개, 

포맷이 FormatA, Mpeg, Avi 3개이므로 각 매체(A~E)Strategy당 하위 클래스가 3개씩 존재해야 한다.

1 + 5 + 5*3 = 21개가 나온다.


이는 매체의 종류가 많아질수록, 포맷의 갯수가 증가될수록 폭발적으로 증가하게 될 것이다.


깊은 트리 구조로 인해 유지보수와 전체적인 구조 파악은 점점 어렵게 될 것이다.



4. 해결방안


현재 문제가 되는 것은, 아이러니하게도 모든 Strategy가 각각 자신의 로직을 스스로 가지고 있다는 것이 문제점이다.


즉, 애초에 Strategy Pattern을 사용하면 유지보수가 힘든 요구사항이 발생한 것이다.


필요한 로직을 각각의 클래스가 가지고있기 때문에, 구현할 로직이 많아진다면 클래스가 많아지는 것이 불가피하다.


따라서 로직만을 한 클래스에서 관리를 하고, 이 로직을 사용하여야 하는 클래스에서 해당 클래스에게 요청하는 방식으로 구현한다면, 클래스의 갯수는 획기적으로 줄어들 것이다.

또한, 어떤 요소 형식인지에 따라 다르게 동작하도록 제어하는 로직을 if-else문 사용하는 등의 사태를 방지하는 수단이 되기도 한다.



5. Solution Code


- Element

public interface Element {
// Visitor를 통해 자신을 제어하기 위한, 방문자를 수용하는, 약속된 메서드.
// Visitor가 구체적인 작업을 대행
public void accept(Visitor visitor);
}

요소 형식의 추상적인 개념인 Element이다. 예제에서는 Media에 해당한다.


 - ConcreteElementA/B

public class ConcreteElementA implements Element {
@Override
public void accept(Visitor visitor) {
System.out.println(this.getClass().getSimpleName() + "# accept(" + visitor.getClass().getSimpleName() + " visitor)");

// Double Dispatching
visitor.visit(this);
}
}
public class ConcreteElementB implements Element {
@Override
public void accept(Visitor visitor) {
System.out.println(this.getClass().getSimpleName() + "# accept(" + visitor.getClass().getSimpleName() + " visitor)");

// Double Dispatching
visitor.visit(this);
}
}

구체적인 요소 형식이다. 예제에서는 Audio/Video에 해당한다.

accept()는 추상적인 개념의 Visitor를 인자로 받아서, visit()함수를 수행하게 된다. 인자로서는 this를 통해 자기 자신을 넘기는데,

'어떤 타입의 ConcreteElement가 넘어갔으니, 해당 메서드를 실행해달라' 라는 의미가 된다.


 - Visitor

// 요소의 형식(ElementA, ElementB)에 따라 수행할 구체적 작업을 방문자에 정의
// 각 요소마다 행동을 달리할 수 있다.
public interface Visitor {

// 대상이 ElementA라면 이러한 로직을 수행
public void visit(ConcreteElementA concreteElementA);

// 대상이 ElementB라면 이러한 로직을 수행
public void visit(ConcreteElementB concreteElementB);
}

Visitor는 각 요소 형식에 따른 로직을 가지고 있다.


 - ConcreteVisitorA/B

public class ConcreteVisitorA implements Visitor {
@Override
public void visit(ConcreteElementA concreteElementA) {
System.out.println(this.getClass().getSimpleName()
+ "# visit(" + concreteElementA.getClass().getSimpleName() + " concreteElementA)");
}

@Override
public void visit(ConcreteElementB concreteElementB) {
System.out.println(this.getClass().getSimpleName()
+ "# visit(" + concreteElementB.getClass().getSimpleName() + " concreteElementB)");
}
}
public class ConcreteVisitorB implements Visitor {
@Override
public void visit(ConcreteElementA concreteElementA) {
System.out.println(this.getClass().getSimpleName()
+ "# visit(" + concreteElementA.getClass().getSimpleName() + " concreteElementA)");
}

@Override
public void visit(ConcreteElementB concreteElementB) {
System.out.println(this.getClass().getSimpleName()
+ "# visit(" + concreteElementB.getClass().getSimpleName() + " concreteElementB)");
}
}

요소 형식(ConcreteElementA/B)에 따라 다른 로직을 수행한다. 메서드는 visit()으로 Overloading/Overriding 하였다.

존재하는 모든 요소 형식을 지원해야 한다.


 - Client

public class Client {
public static void main(String args[]) {
Visitor visitor1 = new ConcreteVisitorA();
Visitor visitor2 = new ConcreteVisitorB();
Element element1 = new ConcreteElementA();
Element element2 = new ConcreteElementB();

element1.accept(visitor1);
element2.accept(visitor1);

element1.accept(visitor2);
element2.accept(visitor2);
}
}


수행 결과는 다음과 같다.



6. Visitor Pattern


Visitor Pattern은 객체의 종류와 로직의 종류가 실행이 N:M관계일 때 클래스의 양이 획기적으로 줄어들게 할 수 있다.


이는 객체의 정의부분과 로직부분을 따로 정의하여 구현이 가능한 경우에 해당한다.


객체의 종류는 Element에 해당되며, 로직의 종류는 Visitor에 해당한다.


이 말은, 객체의 정의부분은 Element부분에, 알고리즘은 Visitor에 기술된다고 할 수 있다.




Visitor는 모든 Element에 대한 로직을 수행할 수 있어야 한다. 


보따리장사꾼(Visitor)이 집(Element)을 돌아가면서 필요한 물건(로직)을 제공한다고 생각하면 쉽다.


모든 집(Element)에 물건(로직)을 제공할 수 있어야 하므로, Element가 추가된다면 Visitor에 해당 Element에서 원하는 로직이 추가되어야 한다.




다른 예를 들자면, 스타크래프트에서 마린, 메딕, 파이어뱃, 드랍쉽 등을 함께 부대로 선택할 수 있다.


여기서 'A'키를 눌러 공격을 한번에 지시한다면, 각 유닛은 자신의 역할에 따라 전부 다르게 행동하여야 할 것이다.



여기서, 모든 유닛의 '공격'이라는 행동에 대한 로직은 Visitor에 전부 기술된다.


마린, 파이어뱃은 Visitor자신의 공격 알고리즘이 기술되어있기 때문에, 해당 Visitor에서 자신의 타입에 대한 공격을 지원하기만 하면 된다.


'공격' 외에도 특수능력, 이동, 정지 등의 Visitor가 존재할 수도 있을 것이다.


따라서 마린, 메딕, 파이어뱃, 드랍쉽 등 Element의 갯수가 N개

각 유닛이 수행할 수 있는 공격, 이동, 정지 등 Visitor의 갯수가 M개 존재해야 한다.


위 경우, 유닛(Element)이 4가지 종류이므로 N의 값은 4가 된다. 또한, 행동(Visitor)이 3가지 종류이므로 M의 값은 3이 된다.




처음부터 Visitor Pattern을 고려하는 것 보단,

1. 객체군과 이 객체군들이 사용하여야 하는 행동의 종류가 N:M의 관계가 되는 경우

2. 1번과 더불어 점점 더 규모가 커지는 경우


Visitor Pattern을 사용한 Refactoring을 고려하는 것이 좋겠다.


또한, JPA&Hibernate 프로그래밍에서 instanceof 를 활용한 Type Checking은 Polymorphic Query에서 사용이 불가능하다.

이런 경우 Visitor Pattern을 사용하여야 한다.


- UML & Sequence Diagram




- Class Diagram




7. 장단점


장점

 - 요소 형식(ElementA/B)에 따라 수행할 알고리즘을 Visitor에 분리하여 구현가능

 - 새로운 연산을 요소 형식(ElementA/B) 변경 없이 추가가능


단점

 - Element의 입장에서, 정보 은닉이 필요한 부분까지 Visitor에게 노출해야 하므로, 신뢰성을 떨어지는 원인이 되기도 함

 - 모든 Visitor는, 모든 Elemnt에 대한 연산을 지원해야 하므로, ConcreteElementC 등 요소 형식이 추가되면, 이 요소 형식에 대한 연산

( visit(ConcreteElementC concreteElementC) )을 추가해야 함. 

'Design Pattern > Behavioral Pattern' 카테고리의 다른 글

Mediator Pattern  (0) 2018.02.16
Chain Of Responsibility Pattern  (0) 2018.02.16
Iterator Pattern / Iterable Interface  (0) 2018.02.15
Template Method Pattern (기본)  (0) 2017.09.26
Observer Pattern  (0) 2017.09.26
Comments