디자인패턴 (Visitor)

백종현·2023년 5월 23일
0

Visitor 패턴

데이터 처리가 한 종류가 아닌 경우 새로운 처리가 필요할 할 때마다 데이터 구조의 클래스를 수정해야 한다.
데이터의 구조와 처리를 분리하여, 데이터 구조 안을 돌아다니는 '방문자'를 나타내는 클래스를 준비하고 그 클래스에 처리를 맡긴다.

이름설명
Visitor파일이나 디렉터리를 방문하는 방문자를 나타내는 추상 클래스
ElementVisitor 클래스의 인스턴스를 받아들이는 데이터 구조를 나타내는 클래스
ListVisitorVisitor의 구현체
EntryFile과 Directory의 상위 클래스가 되는 추상 클래스
File파일 클래스
Directory디렉토리 클래스
Main동작하는 클래스
public abstract class Entry /*implements Element*/ {
    public abstract String getName();	// 이름을 얻는다 
    public abstract int getSize();		// 크기를 얻는다  

    // 문자열 표현 
    @Override
    public String toString() {
        return getName() + " (" + getSize() + ")";
    }
}
public class Directory extends Entry implements Iterable<Entry> {
    private String name;
    private List<Entry> directory = new ArrayList<>();

    public Directory(String name) {
        this.name = name;
    }

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

    @Override
    public int getSize() {
        int size = 0;
        for (Entry entry: directory) {
            size += entry.getSize();
        }
        return size;
    }

    public Entry add(Entry entry) {
        directory.add(entry);
        return this;
    }

    @Override
    public Iterator<Entry> iterator() {
        return directory.iterator();
    }

    /* @Override
    public void accept(Visitor v) {
        v.visit(this);
    } */
}
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 String getName() {
        return name;
    }

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

    /* @Override
    public void accept(Visitor v) {
        v.visit(this);
    } */
}

위의 File과 Directory를 보자. 위 두 가지의 경우의 클래스가 기존에 존재한다고 생각해보자. 여기서 내부적인 데이터 구조(Entry)를 전부 읽어오는 역할을 맡았다고 생각해보자. Visitor 패턴을 사용하지 않는 경우는, 어떻게 구현할까를 생각해보면, Entry에 각 File과 Directory와 같은 데이터 구조를 가지는 클래스에 내부적으로 데이터를 읽어오는 메소드...를 추가하고, 각자에 맞게 이를 구현하려고 할 것이다. 하지만 기존에 존재하는 클래스들을 변경하는 방식은 오류를 일으킬 수 있고, 코드가 손상될 수도 있을 것이다. 이를 위해 외부로 이 데이터 클래스를 읽어오도록 변경을 해보자.

public abstract class Visitor {
    public abstract void visit(File file);
    public abstract void visit(Directory directory);
}
public class ListVisitor extends Visitor {
    // 현재 주목하는 디렉터리 이름
    private String currentdir = "";

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

    // Directory 방문 시 
    @Override
    public void visit(Directory directory) {
        System.out.println(currentdir + "/" + directory);
        String savedir = currentdir;
        currentdir = currentdir + "/" + directory.getName();
        for (Entry entry: directory) {
            entry.accept(this);
        }
        currentdir = savedir;
    }
}
public interface Element {
    public abstract void accept(Visitor v);
}

이와 같이 Visitor라는 파일이나 디렉터리를 방문하는 방문자를 나타내는 추상 클래스를 만들고, 이에 관련해 구현체를 ListVisitor로 만들어 어떻게 데이터 구조를 읽어올 지 (처리할 지) 명세하였다. 그리고 Element라는 인터페이스를 기존의 Entry 객체에 추가하여 Visitor를 받아들이는 방식을 추가하도록 하였다. (위의 File과 Directory에서 주석을 한 부분)

여기서 ListVisitor에서 중요한 부분 중 하나가 이와 관련하여 객체가 어떤 메소드를 사용할 것인지 사전에 알 수 없다. (예를 들어, dir.accept(new ListVisitor());를 실행했다고 생각해보자. 이 경우에 dir 내부의 값이 Directory인지 File인지 확인할 방도가 없다.) 따라서 이를 위해 Directory와 Entry 클래스의 v.visit(this);를 통해 처리하였다. 즉, 이제 accept 함수를 사용하게 되면 잘 처리가 가능해진다.

Visitor 패턴의 요소

Visitor(방문자) 역할 : 데이터 구조의 구체적인 요소를 처리하는(읽는) 부분에 대한 인터페이스를 작성하는 부분이다. 보통 visit(Xxx)로 선언한다. 예제 프로그램에서는 Visitor이다.
ConcreteVisitor(구체적인 방문자) 역할 : Visitor의 인터페이스를 구현한다. visit(Xxx) 형태의 메소드를 구현하고, 각 ConcreteElement마다 처리를 기술한다. 예제 프로그램에서는 ListVisitor이다.
Element(요소)의 역할 : Visitor가 방문할 곳을 나타낸다. 방문자를 받아들이는 accept 메소드를 선언하는 역할을 한다. 예제 프로그램에서는 Element이다.
ConcreteElement(구체적인 요소)의 역할 : Element의 인터페이스 구현한다

더블 디스패치

element.accept(vistor)
visitor.visit(element)

accept 메소드는 다음과 같이 호출되고, visit 메소드는 다음과 같이 호출된다. 이는 정반대 관계에 있다. Visitor 패턴에서는 ConcreteElement역과 ConcreteVisitor 역할의 조합으로 실제 처리를 결정하고, 이를 더블 디스패치라고 한다. 비지터 패턴은 이를 통해 번거로운 조건문 없이 객체에 적절한 메서드를 실행하는 것을 돕는다.

왜 Visitor를 사용할까?

반복 처리가 필요하면 데이터 구조 안에 루프를 사용하면 되지만 이렇게 왜 메소드 호출을 복잡하게 해야할까라는 의문이 든다. 이렇게 분리를 하게 되면 장점이 생긴다. File 클래스와 Directory 클래스의 부품으로서의 독립성을 높여주게 된다.

또한 이는 기능의 확장과 수정에 용이하게 된다. 즉 기존 클래스를 수정하지 않고 확장이 가능하게 해주는 것이다.

비지터 객체는 복잡한 객체 구조(예: 객체 트리)의 모든 요소에 대해 작업을 수행해야 할 때 사용하자!

참조 :
https://wisdom-and-record.tistory.com/89
Java 언어로 배우는 디자인 패턴 입문
https://refactoring.guru/ko/design-patterns/visitor

profile
노력하는 사람

0개의 댓글