Recent Posts
Recent Comments
Link
12-04 00:16
Today
Total
관리 메뉴

삶 가운데 남긴 기록 AACII.TISTORY.COM

JAVA Visitor 패턴 - 자료구조를 돌아다니면서 처리 본문

DEV&OPS/Java

JAVA Visitor 패턴 - 자료구조를 돌아다니면서 처리

ALEPH.GEM 2024. 5. 8. 20:40

Visitor 패턴

개요 및 장점

Visitor 패턴은 형태가 거의 변경되지 않는 객체(데이터 구조)에서 처리 로직이 자주 변경되는 경우, 
객체들의 구조와 알고리즘을 분리하여 적용할 수 있게 해 줍니다. 
이 패턴은 객체의 클래스에서 알고리즘을 분리하여 새로운 알고리즘을 추가하거나 수정할 때 기존 코드를 수정하지 않고도 가능하게 합니다. 

Visitor 패턴은 새로운 기능을 추가하기 위해 기존 객체 구조를 변경할 필요 없이 새로운 방문자 클래스를 추가하면 됩니다.
객체 구조에 대한 다양한 작업을 별도의 방문자 클래스로 분리하여 객체 구조와 방문자 클래스 사이의 결합도를 낮춰 코드 유지 관리를 용이하게 합니다.

 

Visitor 패턴의 활용

  • 컴파일러: 소스 코드를 분석하고 변환하는 과정에서 방문자 패턴을 사용하여 각 문법 요소에 대한 처리를 분리할 수 있습니다.
  • XML 파서: XML 문서를 분석하는 과정에서 방문자 패턴을 사용하여 각 요소에 대한 처리를 분리할 수 있습니다.
  • 그래픽 프로그램: 도형을 그리는 과정에서 방문자 패턴을 사용하여 각 도형에 대한 처리를 분리할 수 있습니다.

 

Visitor 패턴의 구현

  • Element( 방문대상) 인터페이스: 방문자가 방문할 수 있는 모든 객체의 공통 인터페이스를 정의합니다. 이 인터페이스는 accept 메서드를 포함합니다.
  • ConcreteElement 클래스: Element 인터페이스를 구현하는 구체적인 객체 클래스입니다.
  • 방문자 Visitor(방문자) 인터페이스: 여러 객체들을 방문하여 원하는 작업을 수행하기 위해 각 ConcreteElement 클래스를 방문할 때 수행할 작업을 정의하는 인터페이스입니다.
  • ConcreteVisitor 클래스: Visitor 인터페이스를 구현하여 각 ConcreteElement 클래스에 대한 구체적인 작업을 정의합니다.

 

Visitor 패턴의 예제

동물 클래스 구조가 있다고 가정하겠습니다. 
각 동물은 각자의 기능(행동)을 가지고 있을 수 있습니다. 
다음 강아지와 고양이에 대하여 각기 다른 소리를 출력하는 기능을 Visitor 패턴으로 추가해 보겠습니다.

// 방문자(Visitor) 인터페이스
interface Visitor {
    void visit(Dog dog);
    void visit(Cat cat);
}

// 방문 대상(Element) 인터페이스
interface Animal {
    void accept(Visitor visitor);
}

// 방문 대상(Element) 클래스를 구현
class Dog implements Animal {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 또다른 방문 대상(Element) 클래스를 구현
class Cat implements Animal {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 방문자(Visitor) 클래스를 구현
class SoundVisitor implements Visitor {
    @Override
    public void visit(Dog dog) {
        System.out.println("강아지: 멍멍!");
    }

    @Override
    public void visit(Cat cat) {
        System.out.println("고양이: 야옹~");
    }
}

//실행
public class Main {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        Visitor soundVisitor = new SoundVisitor();

        dog.accept(soundVisitor); //멍멍!
        cat.accept(soundVisitor); //야옹~
    }
}

 

 

Double dispatch

element.accept(Visitor);

visitor.visit(element);

이렇듯 element는 visitor를 accept 하고 visitor는 element를 visit 하고 있습니다.

이렇게 한쌍의 element와 visitor에 의한 처리를 double dispatch(이중 분리)라고 합니다.

 

 

Visitor 패턴의 목적과 The Open-Closed Principle

visitor 패턴의 목적은 처리를 데이터 구조에서 분리하기 위함입니다.

즉, 구조는 element가 맡고 처리는 visitor가 맡는 것입니다.

클래스들은 각기 부품으로써 독립성을 높일 수 있습니다.

만약 처리 내용을 element에 기술하면 새로운 처리를 추가하거나 확장할 때 element 클래스를 수정해야 하기 때문에 visitor 패턴이 아닙니다. 

element는 각각의 entry를 얻기 위한 iterator 같은 메서드를 제공할 필요가 있습니다.

그래서 visitor들은 이 entry들을 얻어서 처리를 할 수 있습니다.

 

The Open-Closed Principle은 Robert C.Martin이 C++ Reprot(Jan.1966)에 쓴 Engineering Notebook 칼럼에 정리되어 있습니다.

이 원칙은 extension에는 열려있지만 modification에서는 닫혀있어야 한다는 원칙입니다.

클래스에 대한 수정과 확장 요구는 빈번하게 일어납니다.

확장을 할 때마다 기존의 클래스를 수정하는 것은 곤란합니다.

확장하려면 기존 클래스는 수정하지 않고 클래스를 확장해야 합니다. 

클래스가 부품으로 재사용 가치가 높아지게 하는 객체지향의 목적에 충실한 원칙이라고 할 수 있습니다.

 

 

트리 구조 File, Direcotry의 Visitor 패턴 예제

Visitor 추상 클래스

Visitor.java 클래스는 추상 클래스로 생성합니다.

파일과 디렉터리를 방문하는 역할입니다.

visit(File), visit(Directory)  메서드를 오버로드합니다.

package visitor;

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

 

visit(File)은 File을 방문했을 때 File 클래스가 호출합니다.

마찬가지로 visit(Directory)는 Directory를 방문했을 때 Directory클래스가 호출합니다.

 

Element 인터페이스

Visitor 클래스의 생성자를 받아들이는 데이터 구조를 나타내는 인터페이스입니다.

따라서 accept(Visitor visitor) 메소드를 만들어서 Visitor 객체를 인수로 받습니다.

package visitor;

public interface Element {
	public abstract void accept(Visitor visitor);
}

 

Entry 추상 클래스

File 클래스와 Directory 클래스의 상위 클래스로 Acceptor 인터페이스를 구현한 추상클래스입니다.

여기서는 Element 인터페이스를 implements 합니다.

Entry 클래스를 Visitor 패턴에 적용시키기 위해서입니다.

따라서 실제 accept() 메서드를 구현하는 것은 File클래스와 Directory클래스가 될 것입니다.

package visitor;

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 클래스 

file을 다루기 위한 클래스입니다.

Visitor.visit() 메소드를 호출해서 방문한 File의 인스턴스를 지정해 줍니다.

package visitor;

public class File extends Entry {
	private String name;
	private int size;
	
	public File(String name, int size) {
		this.name = name;
		this.size = size;
	}

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

	@Override
	public String getName() {
		return name;
	}

	@Override
	public int getSize() {
		return size;
	}

}

 

Directory 클래스

directory를 다루기 위한 클래스입니다.

디렉터리의 엔트리를 얻기 위한 Iterator를 반환합니다.

package visitor;

import java.util.ArrayList;
import java.util.Iterator;

public class Directory extends Entry{
	private String name;
	private ArrayList dir = new ArrayList();
	
	public Directory(String name) {
		this.name = name;
	}

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

	@Override
	public String getName() {
		return name;
	}

	@Override
	public int getSize() {
		int size = 0;
		Iterator iterator = dir.iterator();
		while(iterator.hasNext()) {
			Entry entry = (Entry)iterator.next();
			size += entry.getSize();
		}
		return size;
	}
	
	public Entry add(Entry entry) {
		dir.add(entry);
		return this;
	}
	
	public Iterator iterator() {
		return dir.iterator();
	}

}

 

ListVisitor 클래스

Visitor 클래스를 상속받아 데이터 구조(파일과 디렉터리)를 돌아다니면서 그 종류를 나타내기 위한 클래스입니다.

currentdir는 현재 주목하고 있는 디렉토리 이름을 저장합니다.

visit(File) 메서드는 파일을 방문했을 때 File클래스의 accept() 메서드에서 호출됩니다.

즉, visit(File) 메서드는 File 클래스의 인스턴스에 대해서 실행해야 할 처리를 기술합니다.

visit(Directory) 메서드는 디렉터리를 방문했을 때 Directory클래스의 accept() 메서드 내에서 호출됩니다.

마찬가지로, Directory 클래스의 인스턴스에 대해서 실행해야 할 처리를 기술합니다.

accept()메서드는 visit() 메소드를 호출하고 visit() 메소드는 accept() 메서드를 호출합니다.

package visitor;

import java.util.Iterator;

public class ListVisitor extends Visitor {
	private String currentdir = "";	//현재 주목하고 있는 디렉토리

	@Override
	public void visit(File file) {
		System.out.println(currentdir + "/" + file);
	}

	@Override
	public void visit(Directory directory) {
		System.out.println(currentdir + "/" + directory);
		String saveDir = currentdir;
		currentdir = currentdir + "/" + directory.getName();
		Iterator iterator = directory.iterator();
		while (iterator.hasNext()) {
			Entry entry = (Entry)iterator.next();
			entry.accept(this);
		}
		currentdir = saveDir;
	}

}

 

FileTreatmentException

File에 대해 add 하는 경우 발생되는 예외클래스입니다.

package visitor;

public class FileTreatmentException extends RuntimeException {
	public FileTreatmentException() {
		
	}
	public FileTreatmentException(String message) {
		super(message);
	}
}

 

Launcher 클래스

Visitor 패턴을 실행시키는 main() 메서드가 있는 클래스입니다.

package visitor;

public class Launcher {

	public static void main(String[] args) {
		try {
			System.out.println("root entries 들을 만들고 있습니다.");
			Directory rootDir = new Directory("root");
			Directory binDir = new Directory("bin");
			Directory usrDir = new Directory("usr");
			Directory tmpDir = new Directory("tmp");
			rootDir.add(binDir);
			rootDir.add(tmpDir);
			rootDir.add(usrDir);
			binDir.add(new File("vim", 2000));
			binDir.add(new File("start.sh", 300));
			rootDir.accept(new ListVisitor());
			
			System.out.println("root entries 들을 만들고 있습니다.");
			Directory apple = new Directory("apple");
			Directory banana = new Directory("banana");
			Directory grape = new Directory("grape");
			usrDir.add(apple);
			usrDir.add(banana);
			usrDir.add(grape);
			apple.add(new File("red.bmp", 400));
			apple.add(new File("round.jpg", 500));
			banana.add(new File("yellow.png", 600));
			grape.add(new File("span.doc", 700));
			grape.add(new File("over.txt",800));
			rootDir.accept(new ListVisitor());
		}catch(FileTreatmentException e) {
			e.getMessage();
		}
	}

}

 

처리 흐름

  1. ListVisitor 인스턴스 객체 생성
  2. Directory의 인스턴스에 대해 accept() 호출
  3. Directory의 인스턴스는 인수로 전달된 ListVisitor의 visit(Directory) 메서드를 호출
  4. ListVisitor의 인스턴스는 그 디렉터리 안을 조사해서 최초 파일인 accept() 메서드를 호출하면서 인수는 this를 전달
  5. File의 인스턴스는 인수로 전달된 ListVisitor의 visit(File) 메서드를 호출. 
  6. visit(File)과 accept()에서 반환
  7. 다른 File의 인스턴스(두 번째 파일)의 accept() 메서드 호출. 인수는 ListVisitor의 인스턴스 this를 전달 
  8. 동일하게 File의 인스턴스는 visit(File) 메서드를 호출 후 반환

 

 

 

 

 

 

 

 

 

728x90