소프트웨어 개발/Design Pattern

⑬ 디자인 패턴(Design Pattern) - Visitor

늘근이 2015. 9. 26. 23:19

방문자 패턴은, Composite 패턴과 크게 다르지 않다.  내부적으로

예를들어 정말 쉬운 예로, 파일 탐색기를 구현해볼수 있는데 파일 탐색기는 자꾸 여기저기 돌아댕기면서 필요한 파일을 찾아서 자기 호주머니에 그 파일들이 있는지 잘 적어놨다가 방문이 모두 끝나면 사용자에게 결과를 보여줄수 있다.

이 탐색기는 방문자(Visitor)이다. 그리고 이를 받아들이는 놈들은 어떻게 부르는지는 크게 중요하지는 않지만 여기서는 Host로 칭하자. 이 Host들은 방문자의 방문을 받아들이며 accept(Visitor v) 필요한 처리를 해준다.

일단 추상 클래스로 만들어진 Host를 한번 만들어보자.

public abstract class Host{
    public abstract String getName();                                 
    public abstract int getSize();  
    public abstract void accept(Visitor v);
    

    public String toString() {                                         
        return getName() + " (" + getSize() + ")";
    }
    
}

Host의 경우, Visitor를 받아들이는 부분, 그리고 이름과 크기를 리턴하는 getName() 과 getSize() 가 존재한다.


파일들과 디렉토리는 탐색기의 방문을 받아들이는 이 일종의 Host이다. 따라서 추상클래스 Host를  상속받아 구현한다. 먼저 간단한 File부터 상속받아 구현한다.

public class File extends Host {
    private String name;
    private int size;
    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }
    public String getName() {
        return name;
    }
    public int getSize() {
        return size;
    }
    public void accept(Visitor v) {
        v.visit(this);
    }
}

특별한것은 없고, accept() 메서드에서 visitor에 구현되어있는 visit() 메서드를 다시 불러오고 있다. Visitor클래스는 객체의 accept메서드를 호출하고 accept는 다시 visit메서드를 불러오는데, 이는 언뜻보면 불필요한 작업이지만 개발자가 accept메서드를 보유한 모든 클래스에 대해 접근을 할수 있게 해주고 정작 처리는 Visitor를 구현한 놈에서 처리하기 때문에 나름 유용할수 있다.


디렉토리는 사실 복잡한데 Composite패턴에서 보았듯이 막다른 골목이 아니고 내부적으로 다른 디렉터리를 가지고 있을수 있는 구조이기 때문에 내부적인 트리구조를 가질수 있다. 따라서 조금더 복잡하다.

public class Directory extends Host {
    private String name;                   
    private ArrayList dir = new ArrayList();      
    public Directory(String name) {   
        this.name = name;
    }
    public String getName() {         
        return name;
    }
    public int getSize() {            
        int size = 0;
        Iterator it = dir.iterator();
        while (it.hasNext()) {
            Host host = (Host)it.next();
            size += host.getSize();
        }
        return size;
    }
    public Host add(Host host) {     
        dir.add(host);
        return this;
    }
    public Iterator iterator() {      
        return dir.iterator();
    }
    public void accept(Visitor v) {      
        v.visit(this);
    }
}

어떤 부분은 file과 같지만 차이가 나는 부분은 내부적으로 가지고 있는 또다른 파생 디렉토리들을 관리하기 위해 List타입으로 디렉터리를 관리하고 있다는점. 파생된 디렉토리의 사이즈를 구하기 위해 내부적으로 루프를 돌면서 사이즈를 모두 더하고 있다는 점이다.

파일탐색기의 경우, 어떠한 디렉토리의 사이즈를 구하려면 그 안에 있는 모든 파일들을 더해서 보여주는것이 당연하다.


이제 파일 탐색기의 원형인 Visitor를 만들어 보자. 굳이 메서드를 구현할 필요가 없기 때문에 interface로 만들어본다.

public interface Visitor {
    public void visit(File file);
    public void visit(Directory directory);
}

파일탐색기는 단 두개 종류밖에 검색을 하지 못한다. 파일과 디렉토리.. 생각해보면 맞는 사실이다.


이제 이를 상속해서 실제로 ConcreteVisitor를 만들어 보면 된다.

public class SearchVisitor implements Visitor {
    private String filetype;
    private ArrayList found = new ArrayList();
    public SearchVisitor(String filetype) {    
        this.filetype = filetype;
    }
    public Iterator getFoundFiles() {             
        return found.iterator();
    }
    public void visit(File file) {                
        if (file.getName().endsWith(filetype)) {
            found.add(file);
        }
    }
    public void visit(Directory directory) {  
        Iterator it = directory.iterator();
        while (it.hasNext()) {
            Host host = (Host)it.next();
            host.accept(this);
        }
    }
}

내부적으로 찾아준 파일과 디렉터리 리스트를 가지고 있는 List가 존재하며 visit메서드도 두가지 종류로 구현되어있다. File의 경우 찾아지면 filetype과 일치하는 지 확인한 후 내부적으로 찾기결과 리스트에 담아둔다.

디렉터리의 경우 찾아지면 신기하게 작동하는데, 내부적으로 루프를 돌면서 하나하나의 디렉터리를 구한다음에 계속 디렉토리 안쪽을 파고들면서 방문하는 구조로 되어있다.


마지막으로 Main이다.

public class Main {
	public static void main(String[] args) {

		Directory rootdir = new Directory("root");
		Directory bindir = new Directory("bin");
		Directory usrdir = new Directory("usr");
		
		rootdir.add(bindir);
		rootdir.add(usrdir);
		
		bindir.add(new File("vi",  20000));
		bindir.add(new File("top", 40000));

		Directory myDocument = new Directory("my document");
		Directory myVideo = new Directory("my video");
		
		usrdir.add(myDocument);
		usrdir.add(myVideo);
		
		myDocument.add(new File("index.html", 100));
		myDocument.add(new File("favorites.txt", 200));
		myVideo.add(new File("yadong.wmv", 300));

		SearchVisitor searcher = new SearchVisitor(".txt");
		rootdir.accept(searcher);

		System.out.println("텍스트 파일은 다음과 같습니다. : ");
		Iterator it = searcher.getFoundFiles();
		while (it.hasNext()) {
			File file = (File) it.next();
			System.out.println(file.toString());
		}
	}
}

결과는 다음과 같다.


 텍스트 파일은 다음과 같습니다. :
favorites.txt (200)


그렇다면 도대체 왜 이렇게 어렵게 만드느냐? Visitor패턴에서는 구조의 변화가 적지만 신규 기능을 추가하고 싶을때 사용할수 있다. 즉 위에서 Host를 구현한 여러 디렉토리와 파일들은 현재의 OS구조에서는 별로 바뀔것이 없는 부분이다. 다만, 이 파일과 디렉터리를 단순히 찾는 기능외에 압축을 한다든지, 일괄작업을 한다든지 하는부분에 있어서의 기능추가가 필요할수 있다.

원래의 객체지향형태의 프로그래밍은 구조는 마구 상속받아 확장해 나갈수 있지만 기능 측면에서 무언가를 갑자기 코드안에 구겨넣기가 불편할때가 있다. Visitor패턴은 조금 복잡하기 때문에 직관적이지는 않지만 어쨌뜬 구조가 변화가 적고 기능을 쉽게 추가해야 할 필요가 있을 때 유용하게 쓰일수 있을것이다.