제 홈페이지의 모든 글은 anti-nhn license에 따릅니다.



자바 디자인 패턴 16 - Visitor

1. Visitor 패턴은..

복잡한 구조체 안을 돌아다니면서 어떤 일을 해야 할 경우가 있습니다. Visitor는 어떤 구조체에 대해 그 안을 돌아다니면서 어떤 일을 하는 것입니다. 이 때, 구조체 1개에 하는 일이 딱 1개라는 보장은 없습니다. 하나의 구조체에 대해 다양한 일들을 할 수 있습니다. 하고 싶은 일이 추가된다고 해서 구조체를 변경하는 것은 무리입니다. 이런 때는 Visitor를 추가하면 됩니다. 예제에서는 PC의 디렉토리-파일 구조에 대해 야동을 찾는 일을 하는 Visitor를 구현해보았습니다.

2. 예제

--------- Component, Composite, Leaf 등은 Composite 패턴 설명에 썼던 것을 거의 그대로 사용했습니다. 바뀐 부분은 색깔 처리했습니다. 처리한 부분만 보시면 됩니다.


package ch16_Visitor;
import java.util.ArrayList;
import java.util.List;

public abstract class Component implements Acceptor{
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    private String componentName;
    protected List<Component> children = new ArrayList<Component>();
    public Component(String componentName) {
        this.componentName = componentName;
    }
    public String getComponentName() {
        return componentName;
    }
    public abstract void add(Component c);
    public List<Component> getChildren(){
        return children;
    }
}

package ch16_Visitor;
public class Composite extends Component {
    public Composite(String componentName) {
        super(componentName);
    }
    @Override
    public void add(Component c) {
        children.add(c);
    }
}

package ch16_Visitor;
public class Leaf extends Component{
    public Leaf(String componentName) {
        super(componentName);
    }
    @Override
    public void add(Component c) {
        throw new UnsupportedOperationException();
    }
}
------------------ Visitor를 받아들일 수 있는 구조체 ----------------
package ch16_Visitor;

public interface Acceptor {
    void accept(Visitor visitor);
}
------------------ Acceptor를 방문하는 Visitor----------------
package ch16_Visitor;

public interface Visitor {
    void visit(Acceptor acceptor);
}
------------------ 야동 찾는 YadongFinder ----------------
package ch16_Visitor;
import java.util.ArrayList;
import java.util.List;
public class YadongFinder implements Visitor {
    private List<String> yadongList = new ArrayList<String>();
    private List<String> currentList = new ArrayList<String>();
    public void visit(Acceptor acceptor) {
        if (acceptor instanceof Composite) {
            Composite composite = (Composite) acceptor;
            currentList.add(composite.getComponentName());
            List<Component> children = composite.getChildren();
            for (Component component : children) {
                component.accept(this);
            }
            currentList.remove(currentList.size()-1);
        }else  if (acceptor instanceof Leaf) {
            Leaf leaf = (Leaf) acceptor;
            doSomething(leaf);
        }
    }
    protected void doSomething(Leaf leaf){
        if (isYadong(leaf)) {
                String fullPath = getFullPath(leaf);
                yadongList.add(fullPath);
            }
    }
    protected String getFullPath(Leaf leaf) {
        StringBuilder fullPath = new StringBuilder();
        for (String element : currentList) {
            fullPath.append(element).append("\\");
        }
        return fullPath.append(leaf.getComponentName()).toString();
    }
    private boolean isYadong(Leaf leaf) {
        return leaf.getComponentName().endsWith(".avi");
    }

    public List<String> getYadongList() {
        return yadongList;
    }
}
------------------ 테스트 클래스 ---------------- 
package ch16_Visitor;

public class Test {
    public static void main(String[] args) {
        Composite main = createComposite();
        YadongFinder visitor = new YadongFinder();
        visitor.visit(main);
        for (String string : visitor.getYadongList()) {
            System.out.println(string);
        }

    }

    private static Composite createComposite() {
        Composite main = new Composite("C:");
        Composite sub1 = new Composite("Program Files");
        Composite sub2 = new Composite("WINDOWS");
        Composite sub11 = new Composite("Pruna");
        Composite sub21 = new Composite("system32");
        Composite sub111= new Composite("Incoming");

        Leaf leaf1111 = new Leaf("강호동 닮은여자-짱이쁨.avi");
        Leaf leaf1112 = new Leaf("EBS야동특강.avi");
        Leaf leaf211 = new Leaf("야메떼-다이조부.avi");
        Leaf leaf212 = new Leaf("이건 야동아님.jpg");
       
        main.add(sub1);
        main.add(sub2);
        sub1.add(sub11);
        sub2.add(sub21);
        sub11.add(sub111);

        sub111.add(leaf1111);
        sub111.add(leaf1112);
        sub21.add(leaf211);
        sub21.add(leaf212);
        return main;
    }
}
---------------- 테스트 결과 -------------
C:\Program Files\Pruna\Incoming\강호동 닮은여자-짱이쁨.avi
C:\Program Files\Pruna\Incoming\EBS야동특강.avi
C:\WINDOWS\system32\야메떼-다이조부.avi

위의 예제에서 중요한 것은 Visitor의 visit(Acceptor) 와  Acceptor의 accept(Visitor)  입니다.

Visitor는 visit(Acceptor) 를 가지고 있고, Acceptor는 accept(Visitor) 를 가지고 있습니다. 둘의 차이가 헤깔립니다. 게다가 accept(Visitor) 를 구현해 놓은 것을 보면 아래와 같이 그냥 Visitor한테 자기 자신을 던져버리는 게 하는 일의 전붑니다.

public void accept(Visitor visitor) {
    visitor.visit(this);
}

이렇게 해 놓은 이유는 구조체를 돌아다닐 수 있게 하기 위한 것입니다. 실제로 구조체를 돌아다니는 일은 Visitor에서 담당하게 됩니다. 만약 이렇게 해놓지 않았다면, Visitor 안에서 구조체를 돌기 위해 재귀적인 호출을 해야만 복잡한 구조를 다 돌 수 있습니다. YadongFinder의 visit(Acceptor) 를 보면, 재귀적인 호출은 없습니다. 일반적으로 accept(Visitor)의 구현은 위와 같으며 달라질 일이 거의 없습니다.

Visitor의 visit(Acceptor)나 Acceptor의 accept(Visitor) 중 하나는 구조체를 도는 역할을 해야합니다. 구조체를 도는 역할은 Visitor의 visit(Accept)에 맡기는 것이 좋습니다. 구조체를 돌면서 하는 일 뿐만 아니라 "구조체를 도는 방법"도 다른 게 할 수도 있기 때문입니다. 위의 예제에서 YadongFinder는 아무래도 엄마가 짠 것 같습니다. 만약에 아들이 짰다면 구조체를 돌 때 "C:\Program Files\Pruna\Incoming\" 과 같은 비밀스러운 디렉토리는 슬쩍 뛰어넘는 로직을 짤 수 있었겠지요.

그러나 일반적으로 순화하는 로직은 거의 바뀌지 않습니다. 위의 예제에서는 YadongFinder를 상속 받아 doSomething(Leaf) 만 override 하면 뭔가 새로운 Visitor를 만들 수 있습니다.

3. 기타

visit(Acceptor) 안 쪽에서 instance of 로 어떤 클래스인지를 찾아냅니다. 이것은 visit(Composite) 와 visit(Leaf)로 분리시켰더라면 굳이 instance of를 쓸 필요가 없었을 것입니다. 하지만, 그렇게되면 Acceptor라는 인터페이스가 무의미해집니다.

Visitor와 Acceptor는 매우 밀접하게 묶여있습니다. 하지만 사실 Visitor의 구현체와 구조체 사이도 꽤 끈끈하게 묶여있습니다. YadongFinder 안에서 instance of로 체크한 것은 전부 구조체에서 정의된 클래스들(Composite, Leaf) 입니다.

테스트 코드의 YadongFinder를 선언하는 부분을 보면,
YadongFinder visitor = new YadongFinder();  와 같이 되어있습니다. 왜
Visitor visitor = new YadoingFinder(); 라고 인터페이스로 정의를 하지 않았을까요?

YadongFinder의 getYadongList() 부분이 포인트입니다. visitor는 구조체를 돌아다니면서직접 일을 할 수도 있습니다.( 예를들어, YadongFinder를 조금만 수정하면 YadongRemover 로 만들 수도 있습니다.) 하지만 돌아다니면서 뭔가를 수집하는 것과 같이 직접 구조체를 수정하지 않고 단지 정보만 수집하는 경우가 있는데, 그런 경우는 수집한 정보를 다시 뽑아서 사용할 수 있는 방법을 제공해야 합니다. Test 클래스에서 야동 리스트를 찍는 부분을 보시면 됩니다.

by 삼실청년 | 2009/04/08 00:29 | 컴터질~ | 트랙백 | 핑백(2) | 덧글(6)

트랙백 주소 : http://iilii.egloos.com/tb/4906850
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Linked at java design patt.. at 2017/05/31 09:26

... 16. 자바 디자인 패턴 16 – Visitor ... more

Linked at 자바 디자인 패턴 종류 &#8.. at 2017/09/26 09:15

... 자인 패턴 13 – Flyweight 14. 자바 디자인 패턴 14 – Builder 15. 자바 디자인 패턴 15 – Mediator 16. 자바 디자인 패턴 16 – Visitor 출처 : http://iilii.egloos.com 글 내비게이션 Previous Previous post: 기술&#8230; ... more

Commented by 레브민호 at 2009/09/01 13:57
안녕하세요. 깔끔하게 정리해놓으신 글들 잘 보고 있습니다.
이번에 저희팀에서 visitor패턴을 활용하고 있는데요
팀원들에게 visitor의 장점은 뭐고 왜 꼭 써야 하는지 필역하기가 쉽지 않네요. ㅋㅋ
재귀함수 같은데 굳이 왜 써야 하냐는 의견들이 많아서요 ㅎ
하지만 어느정도 프로젝트가 진행되고 확장을 고려해야 하는 시점이 오면 다들 이해하게 되겠죠? ^^
Commented by 삼실청년 at 2009/09/06 21:51
흠.. 위에서도 대충 정리했지만, 구조체와 그 구조체에 대해서 하는 일을 분리시키는 것이 포인트죠.
구조체에 대해서 하는 일이 여러가지일 경우 특히 그러합니다. 재귀 형식의 코드를 각각의 하는 일마다 만드는 것은 낭비죠.
잘 설득하셔서 좋은 프로그램 만드시길 기원합니다~
Commented by limecode at 2013/11/28 15:28
visitor는 이해하기가 많이 어렵네요.
Commented by 삼실청년 at 2013/12/11 01:04
composite을 먼저 정확히 이해하셔야 하구요. 아무래도 구조체에 대한 것이다 보니 소스가 간결하게 안 나오더군요. 저도 저거 소스를 어떻게 하면 줄일 수 있을까 열심히 고민했는데, 결국 저정도가 한계인 것 같습니다.ㅜㅜ
Commented by 베어곰 at 2015/01/15 21:17
안녕하세요
어제 우연히 검색링크(구글) 타고 들어와서 패턴 글읽다가 팬이 되어버렸습니다

'아... 이사람은 천재다!!'

어찌 이렇게 쉽게 설명할 수 있는가? 하는 생각에 오늘도 보고 있습니다

이글을 언제 보실지 모르겠지만 그래도 질문하나 드려봅니다.

YadongFinder visitor = ~~;
visitor.visit(main);

이부분 보고 한참을 고민했습니다

헷갈렸거든요

visitor.visit(main);
main.accept(visit);

결론은 어떻게 해도 결과는 동일하다 인것 같습니다만... 성능상의 아주아주 미세한 차이가 있을 것 같긴 합니다.

어떤가요? 맞는 말인가요?

10년 뒤에라도 답변 부탁드립니다 ^^
Commented by 삼실청년 at 2015/01/22 21:28
안녕하세요. 말씀하신대로 별 차이 없습니다.. 말로 따지자면, 능동태와 수동태 정도의 차이라고 볼 수 있을 것 같습니다.
visitor.visit(main) 이란 것을 말로 풀면, "visitor가 main을 visit해라." 라는 의미일 것이고,
main.accept(visitor) 라는 것은 "main은 visitor에 의해 visit 당해라"라는 의미인데,

말로 봤을 때, visitor.visit 가 좀 더 명확하게 보이지 않을까 싶습니다.

객체지향에서 xxx.foo(yyy) 와 같은 식의 함수가 호출될 때, xxx가 주로 주어, foo가 동사 yyy가 목적어 의 형태인 게 가장 깔끔해 보이는 듯해요. 물론 이런식으로 표현할 수 없는 여러 가지 경우가 있지만 정확히 이런 식으로 표현 가능한 것은 수동의 의미보다는 능동의 의미로 표현하는 게 코드를 보는 사람이 코털만치라도 더 쉽게 읽을 수 있는 것 같습니다. 순전히 제 생각이에요.

찾아주셔서 감사합니다.

:         :

:

비공개 덧글

◀ 이전 페이지          다음 페이지 ▶