[디자인 패턴] Chapter 13. Visitor 패턴

6 분 소요

01. Visitor 패턴

  • 데이터 구조 안에 저장되어 있는 많은 요소에 대해서, 무언가 “처리”해 나가고자 한다
    • 얼핏 생각하면, 데이터 구조를 나타내고 있는 클래스 안에 “처리”를 기술해야 한다고 생각할 것이다
    • 그러나, “처리”가 여러 종류라고 한다면?
      • 새로운 처리가 필요해질 때마다, 데이터 구조를 나타내는 클래스를 수정해야 한다
  • 데이터 구조와 처리를 분리하자
    • 데이터 구조를 돌아다니면 “방문자”를 정의해서, 이 방문자가 “처리”를 담당하도록 하자
    • 새로운 처리를 추가하고 싶을 때는, 새로운 “방문자”를 만든다
  • 데이터 구조는, 문을 두드리는 “방문자”를 받아들이기만 하면 된다

02. 예제 프로그램

  • 방문자가 방문하는 데이터 구조
    • 11장 Composite 패턴의 예제에서 사용된 파일과 디렉토리를 다시 사용한다
  • 파일과 디렉토리로 구성된 데이터 구조를 방문자가 방문하면서, 파일 리스트를 출력한다
이름 해설
Visitor 파일이나 디렉토리를 방문하는 방문자를 나타낸 추상 클래스
Element Visitor 클래스의 인스턴스를 받아들이는 데이터 구조를 나타내는 인터페이스
ListVisitor Visitor 클래스의 하위 클래스로 파일이나 디렉토리의 종류를 나타내는 클래스
Entry File과 Directory의 상위 클래스가 되는 추상 클래스(Element 인터페이스를 구현)
File 파일을 나타내는 클래스
Directory 디렉토리를 나타내는 클래스
FileTreatmentException File에 대해서 add한 경우에 발생하는 예외 클래스
Main 동작 테스트용 클래스

Visitor 클래스

  • 방문자를 나타내는 추상 클래스
  • visit(File)
    • File을 방문했을 때 File 클래스가 호출하는 메소드
  • visit(Directory)
    • Directory를 방문했을 때 Directory 클래스가 호출하는 메소드
  • 메소드 오버로드(overload) 이용했음
public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Directory directory);
}

Element 인터페이스

  • 방문자를 받아들이는 클래스를 위한 인터페이스
  • accept(Visitor v)
    • Visitor 타입을 입력 인자로 받아들임
public interface Element {
    public abstract void accept(Visitor v);
}

Entry 클래스

  • Composite 패턴에 등장했던 File이나 Directory를 위한 추상 클래스
  • Element 인터페이스를 구현함
  • add()
    • Directory 클래스에서만 add()가 유효하므로, Entry 클래스에서는 에러로 처리한다
  • iterator()
    • 요소를 얻을 때 사용하는 메소드
    • Directory 클래스에서만 iterator()가 유효하므로, Entry 클래스에서는 에러로 처리한다
import java.util.Iterator;

public abstract class Entry implements Element {
    public abstract String getName();
    public abstract int getSize();
    public Entry add(Entry entry) throws FileTreatmentException {
	throw new FileTreatmentException();
    }
    public Iterator iterator() throws FileTreatmentException {
	throw new FileTreatmentException();
    }
    public String toString() {
	return getName() + "(" + getSize() + ")";
    }
}

File 클래스

  • accept(Visitor)
    • 클라이언트가, File 객체에게 “방문자를 받아들이세요”라고 요청할 때 호출하는 메소드
      • 클라이언트는 Visitor를 매개변수로 하여 accept를 호출할 것이다
    • 입력 인자로 들어온 방문자의 visit 메소드를 호출한다
      • 이 때, 현재 자신 객체를 인자로 하여 호출한다
      • 그러면, Visitor의 visit(File) 메소드가 실행된다
      • 즉, 방문자가 방문하면 방문자에게 “나는 File 객체입니다. 나의 일을 처리해 주세요”라고 요쳥하는 것과 비슷하다
public class File extends Entry {
    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);
    }
}

Directory 클래스

  • iterator()
    • 디렉토리가 유지하고 있는 엔트리들에 대한 Iterator를 반환한다
  • accept(Visitor)
    • 입력 인자로 들어온 Visitor에게, 자기 자신을 매개 변수로 해서 visit()를 호출한다
    • 그러면, Visitor의 visit(Directory) 메소드가 실행된다
      • 방문자가 방문하면 방문자에게 “나는 Directory 객체입니다. 나의 일을 처리해 주세요”라고 요청하는 것과 비슷하다
import java.util.Iterator;
import java.util.ArrayList;

public class Directory extends Entry {
    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()) {
	    Entry entry = (Entry)it.next();
	    size += entry.getSize();
	}
	return size;
    }
    public Entry add(Entry entry) {
	dir.add(entry);
	return this;
    }
    public Iterator iterator() {
	return dir.iterator();
    }
    public void accept(Visitor v) { // 방문자 승낙
	v.visit(this);
    }
}

ListVisitor 클래스

  • Visitor 클래스의 하위 클래스
  • 실제 데이터 구조(File이나 Directory)를 옮겨 다니면서, 리스트를 출력하는 일을 한다
  • currentdir 필드
    • 현재 주목하고 있는 디렉토리 명을 저장함
  • visit(File)
    • 입력인자로 받아들인 “File 에 대해 수행해야 할 처리”가 기술되어 있다
    • 알고리즘
      • 현재 디렉토리와 File의 toString() 반환 값을 연결하여
      • 현재 파일의 전체 경로를 출력한다
  • visit(Directory)
    • 입력 인자로 받아들인 “Directory에 대해 수행해야 할 처리”가 기술되어 있다
    • 알고리즘
      • 먼저, 디렉토리 전체 경로를 출력한다
      • 현재 디렉토리(currentdir)를 임시로 savedir에 저장한다
      • 현재 디렉토리(currentdir)를, 입력 인자로 들어온 디렉토리로 바꾼다
      • 입력 인자로 들어온 디렉토리의 iterator를 얻는다
      • 입력 인자로 들어온 디렉토리가 유지하는 원소드를 차례로 방문하면서, accept(this)를 호출하여 방문자가 방문했음을 알린다
      • while 루프가 끝나면, currentdir을 원래 디렉토리로 복귀시킨다
    • 복잡한 재귀적인 호출
      • accpet 메소드는 visit 메소드를 호출하고, visit 메소드는 accept 메소드를 호출한다
import java.util.Iterator;

public class ListVisitor extends Visitor {
    private String currentdir = "";
    public void visit(File file) {
	System.out.println(currentdir + "/" + file);
    }
    public void visit(Directory directory) {
	System.out.println(currentdir + "/" + directory);
	String savedir = currentdir;
	currentdir = currentdir + "/" + directory.getName();
	Iterator it = directory.iterator();
	while (it.hasNext()) {
	    Entry entry = (Entry)it.next();
	    entry.accept(this);
	}
	currentdir = savedir;
    }
}

FileTreatmentException 클래스

  • File 엔트리에 무엇인가 추가(add)하고자 할 때 발생되는 예외
public class FileTreatmentException extends RuntimeException {
    public FileTreatmentException() {
    }
    public FileTreatmentException(String msg) {
	super(msg);
    }
}

Main 클래스

  • Composite 패턴에서는,
    • main() 메소드가 엔트리 리스트를 출력하기 위해서 rootdir.printList()를 호출하였다
    • 엔트리의 내용을 출력하는 책임을, File이나 Directory 클래스가 가지고 있다
  • Visitor 패턴에서는,
    • main() 메소드가 엔트리 리스트를 출력하기 위해서, rootdir.accept(new ListVisitor())를 호출하였다
    • 엔트리의 내용을 출력하는 책임을, ListVisitor 클래스가 가지고 있다
public class Main {
    public static void main(String[] args) {
	try {
	    System.out.println("Making root entries...");
	    Directory rootdir = new Directory("root");
	    Directory bindir = new Directory("bin");
	    Directory tmpdir = new Directory("tmp");
	    Directory usrdir = new Directory("usr");
	    rootdir.add(bindir);
	    rootdir.add(tmpdir);
	    rootdir.add(usrdir);
	    bindir.add(new File("vi", 10000));
	    bindir.add(new File("latex", 20000));
	    rootdir.accept(new ListVisitor());

	    System.out.println("");
	    System.out.println("Making user entries...");
	    Directory Kim = new Directory("Kim");
	    Directory Lee = new Directory("Lee");
	    Directory Park = new Directory("Park");
	    usrdir.add(Kim);
	    usrdir.add(Lee);
	    usrdir.add(Park);
	    Kim.add(new File("diary.html", 100));
	    Kim.add(new File("Composite.java", 200));
	    Lee.add(new File("memo.tex", 300));
	    Park.add(new File("gmae.doc", 400));
	    Park.add(new File("junk.mail", 500));
	    rootdir.accept(new ListVisitor());
	} catch (FileTreatmentException e) {
	    e.printStackTrace();
	}
    }
}

Visitor와 Element의 상호 호출

  • 하나의 Directory와 두 개의 File이 있는 경우의 시퀀스 다이어그램

03. 등장 역할

  • Visitor(방문자)의 역할
    • 데이터 구조 내의 각각의 구체적인 요소(ConcreteElement 역할)에 visit(xxx) 메소드를 선언하는 역할
    • visit(xxx) 메소드는, 실제 xxx를 처리하기 위한 실제 코드가 하위 클래스에 의해 제공된다
    • 예제에서는 Visitor 클래스가 해당됨
  • ConcreteVisitor(구체적 방문자)의 역할
    • Visitor 역할의 인터페이스(API)를 실제로 구현하는 역할
    • 예제에서는 ListVisitor 클래스가 해당됨
  • Element(요소)의 역할
    • Visitor 역할이 방문할 장소를 나타내는 역할
    • 방문자를 받아들이는 accept(Visitor) 메소드를 선언한다
    • 예제에서는, Element 인터페이스가 해당됨
  • ConcreteElement(구체적인 요소)의 역할
    • Element 역할의 인터페이스를 구현하는 역할
    • 예제에서는, File이나 Directory 클래스가 해당됨
  • ObjectStructure(객체의 구조)의 역할
    • Element 역할을 집합으로 취급할 수 있도록 해 주는 메소드를 제공한다
    • 예제에서는, Directory 클래스가 해당됨
      • 유지하고 있는 요소들을 얻어갈 수 있도록, iterator 메소드를 제공함
  • 클래스 다이어그램

  • 더블 디스패치(dispatch)
    • dispatch: 급파하다, 발송하다, 신속히 처리하다
    • Visitor의 Element는 서로 대응 관계
    • ConcreteElement와 ConcreteVisitor의 역할을 하는 한 쌍에 의해 실제의 처리가 결정된다

04. 독자의 사고를 넓혀주는 힌트

  • 왜 이렇게 복잡한 일을 하는가?
    • “반복 처리가 필요하면 데이터 구조 안에 루프를 쓰면 되지 않나?”
    • Visitor 패턴의 목적은, “처리”를 “데이터 구조”로부터 분리하는 것이다
      • 다른 처리를 하는 ConcreteVisitor를 추가할 수 있다
      • 또, 기존의 ConcreteVisitor의 기능을 확장하기도 쉽다
      • 결국, 부품의 독립성을 높여준다
  • Open-Closed Principle
    • 확장에 대해서는 열려있고
      • 클래스를 설계할 때에 특별한 이유가 없는 한 장래의 확장을 허락해야 한다
    • 수정에 대해서는 닫혀있다
      • 확장을 하더라도 기존의 클래스는 수정할 필요가 없어야 한다
  • ConcreteVisitor 역할의 추가는 간단하지만, ConcreteElement 역할의 추가는 곤란하다

05. 관련 패턴

  • Iterator 패턴
  • Composite 패턴
  • Interpreter 패턴(23장)

06. 요약

  • 데이터 구조 안을 돌아다니면서 처리를 수행하는 Visitor 패턴

연습 문제

  • 13-1 : FileFindVisitor 클래스 추가하기
    • 지정된 확장자의 파일을 모으는 방문자
  • 13-2 : SizeVisitor 클래스 도입하기
    • “사이즈를 얻는 처리”를 하는 방문자
    • Directory의 getSize 메소드를 응용할 것

Homework : Visitor 패턴 응용

  • FileNameFindVisitor 클래스 추가하기
    • 파일 이름에 “생성자의 입력 인자 name”이 포함된 모든 파일을 모으는 일을 하는 방문자
    • 구현 방법
      • 연습문제 13-1 응용하기
      • 생성자 FileNameFindVisitor(String name)
        • name 문자열이 파일 이름에 포함된 파일을 찾고자 함(String 클래스의 메소드 이용할 것)
    • FileNameFindVisitor.getFoundFiles() 메소드
      • FileNamedFindVisitor가 모은 모든 파일에 대한 Iterator를 얻어올 때 호출하는 메소드
    • Main 클래스
      • Main.main()은 리스트 13-9와 같은 형태
      • 그림 13-2와 같은 디렉토리 구조에, “Kim”, “Lee”, “Park” 디렉토리 안에 각각 “hyejaKim.txt”, “Leehyeja.txt”, “Parkhyeja.tat” 파일을 추가한다
      • 그리고 나서, 파일 이름이 “hyeja”를 포함하는 파일을 모두 출력한다

  • 숙제 제출 방법
    • 보고서
    • 표지 (과목명, 학번, 이름, 제출일, 담당교수 등)
    • 코드 설명 (표를 이용하는 등 보기 좋게 작성)
      • 각 클래스가 하는 일
      • 각 클래스의 각 메소드가 하는 일
    • 코드 프린트

    • 프로그램
    • 해람인의 e참뜰 과제물 제출 기능을 이용
      • 바이러스 체크 후 압축파일로 제출
      • 각 클래스의 소스 파일 (java)
      • 모든 클래스 컴파일 한 파일 (class)

댓글남기기