[OOP] Visitor 패턴 정리하기

2022. 12. 9. 11:07몰랐던거/JAVA & SPRING

1. 비지터(Visitor) 패턴이란?

< Visitor의 사전적 의미 >

visitor의 사전적 의미는 방문객, 손님이라는 의미이다.

그렇다면 visitor가 어떤 패턴인지에 대해 알아보자.

< Visitor 패턴이란? >

비지터 패턴은 데이터 구조와 데이터 처리를 분리해주는 패턴이다.

어떤 데이터를 나타내는 클래스가 있는 경우, 해당 클래스를 처리하는 메서드는 클래스 내부에 있는 것이 편하다.

하지만 비지터 패턴은 메서드가 아닌 별도의 클래스로 구현한다. 데이터 구조와 처리를 분리해줌으로써 새로운 처리 방법이 도입이 되었을 때 기존 소스코드 변경없이 새로운 코드 추가만으로 구현 가능하다.

그리고 데이터 구조는 컴포지트 패턴을 사용함으로써 단일 데이터와 단일 데이터로 구성되는 집합 데이터를 표현할 수 있다.

< Visitor 패턴 간단한 예시 >

어떤 도로의 정보를 xml 형식으로 내보는 행위를 구현해야한다. 이를 위해서 각 클래스 내부에 xml 형식으로 데이터를 내보내는 메서드를 만들었다.

하지만 갑자기 JSON도 추가되어야 할 것 같다고 한다면 모든 메서드를 바꿔야 한다. 또 해당 노드 클래스들에 xml 내보내기 메서드를 넣는 것이 적합할까? 해당 노드 클래스들은 지리 작업을 처리하는 것이 주된 작업인데 xml 내보내기는 좀 어색하다. 이렇게 메서드를 클래스 내부로 통합하는 방법 대신 비지터 클래스로 분리해서 사용하면 책임도 분리되고 확장성도 커진다.

하지만 위와 같이 그래프를 다룬다고 가정했을 때 그래프 탐색 알고리즘을 사용하면서 xml 형식으로 바꾸는 작업을 해야할 텐데 어떻게 그에 맞는 Visitor 구현체를 호출할까?

foreach (Node node in graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node)
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node)
    // …
}

해당 코드 처럼 조건문을 쭉 나열해서 찾는 것은 너무 지저분하다.

그래서 비지터 패턴에선 더블 디스패치 라는 방법을 이용해서 해결한다. 번거로운 조건문 없이 적절한 메서드를 실행할 수 있게 위 예시코드의 City, Industry 와 같은 노드 클래스에서 선택하는 메서드를 만드는 것이다.

foreach (Node node in graph)
    node.accept(exportVisitor)

// City
class City is
    method accept(Visitor v) is
        v.doForCity(this)

이런 느낌으로 객체에서 구현된 accept를 이용해서 Visitor의 알맞는 visit 메서드를 실행한다.


2. 비지터 패턴의 예제 코드

< Application >

public class Application {
    public static void main(String[] args) {
        Visitor visitor = new JsonVisitor();
        City city = new City("053", "대구", 2366852);
        Industry industry = new Industry(1000, "정보통신", "이동통신, 신호처리");

        List<Node> nodes = Arrays.asList(city, industry);
        nodes.forEach(node -> node.accept(visitor));
    }
}
  • 애플리케이션이 돌아가는 부분이다.
  • city, industry 인스턴스를 생성하고 forEach를 돌리면서 많은 조건문 없이 깔끔하게 해당 클래스에 맞는 visit 메서드가 돌아가게 만든다. → 더블 디스패치

< Node 즉 Entity 객체들과 관련있는 부분 >

- Node

public interface Node {
    void accept(Visitor visitor);
}
  • 인터페이스로 구현되었고 구현체들에서 자신에게 맞는 visit 메서드를 실행하기 위해 필요한 accept 메서드를 가지고 있다.

- City

public class City implements Node{

    private final String id;
    private final String name;
    private final int population;

    public City(String id, String name, int population) {
        this.id = id;
        this.name = name;
        this.population = population;
    }

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

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getPopulation() {
        return population;
    }
}
  • Entity 객체이고 자신에게 맞는 속성을 가지고 있고 적합한 visit 메서드를 선택하기 위한 accept 메서드를 구현하고있다.

- Industry

public class Industry implements Node{

    private final int id;
    private final String name;
    private final String subjects;

    public Industry(int id, String name, String subjects) {
        this.id = id;
        this.name = name;
        this.subjects = subjects;
    }

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

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getSubjects() {
        return subjects;
    }
}
  • City와 같은 부분이다.

< Visitor 관련 부분 >

- Visitor

public interface Visitor {
    void visitCity(City city);
    void visitIndustry(Industry industry);
}
  • 인터페이스로 구성되어있고 각각의 Entity를 어떻게 처리해야 하는 지를 위한 visit 메서드를 Entity에 맞게 가지고 있다.

- JsonVisitor

public class JsonVisitor implements Visitor{
    @Override
    public void visitCity(City city) {
        System.out.println("{");
        System.out.println("    id : " + city.getId());
        System.out.println("    name : " + city.getName());
        System.out.println("    population : " + city.getPopulation());
        System.out.println("}");
    }

    @Override
    public void visitIndustry(Industry industry) {
        System.out.println("{");
        System.out.println("    id : " + industry.getId());
        System.out.println("    name : " + industry.getName());
        System.out.println("    subjects : " + industry.getSubjects());
        System.out.println("}");
    }
}
  • Visitor를 구현하고 있고 각각의 Entity를 어떻게 처리해야 하는지에 대한 로직이 들어있다.
  • 만약 다른 형식으로의 처리를 원한다면 XmlVisitor와 같이 구현체를 구성한다면 기존 코드를 수정하지 않고도 다른 형식들을 추가할 수 있다.

< 실행결과 >

실행결과 구현체인 JsonVisitor가 원하는 Json 형태로의 데이터가 출력이 되었고 각각의 Entity 마다 그에 맞는 속성들이 출력된 것을 확인할 수 있다.

⇒ 비지터 패턴이 원하는 목적은 데이터의 구조와 데이터의 처리를 분리하는 것이었고 Entity에 이런 형식 처리 로직을 넣는 것은 많은 책임을 주게 된다. (기존 Entity는 해당 Entity에 맞는 로직을 수행하고 있는데 형식으로 변환하는 로직까지 준다면 책임이 커진다.)

이를 방지할 수 있고 구조와 처리를 분리함으로써 원하는 데이터 처리 형식이 추가되어도 확장성있고 변화엔 닫히게 구현이 가능해진다.

  • 예제 코드
design-patterns/src/main/java/com/programmers/java/behavioral_patterns/visitor at main · twotwobread/design-patterns
You can't perform that action at this time. You signed in with another tab or window. You signed out in another tab or window. Reload to refresh your session. Reload to refresh your session.
https://github.com/twotwobread/design-patterns/tree/main/src/main/java/com/programmers/java/behavioral_patterns/visitor


3. 비지터 패턴에 관한 추가적인 정보

< 적용 >

  • 비지터 객체는 복잡한 객체 구조 (ex: 객체 트리)의 모든 요소에 대해 작업을 수행해야 할 때 사용하세요.
    • 비지터 객체가 모든 대상 클래스들에 해당하는 같은 작업의 여러 변형들을 구현하도록 함으로써 다양한 클래스들을 가진 여러 객체의 집합에 작업을 실행할 수 있도록 해준다.
    • 즉, accept 메서드를 가진 인터페이스를 객체들에 구현시키면 해당 객체들의 집합을 돌리면서 accept만 실행 시키면 원하는 작업을 수행할 수 있기에 복잡한 구조에서 되게 편리할 것 같다.
  • 비지터 패턴을 사용해서 보조 행동들의 비즈니스 로직을 정리하세요.
    • 해당 패턴은 앱의 주 클래스들의 주 작업들을 제외한 모든 다른 행동들을 비지터 클래스들의 집합으로 추출함으로써 그들이 주 작업에 더 집중하도록 만들 수 있게 한다.
    • 즉, 구조와 처리를 분리한 것처럼 굳이 객체 내에 있지 않아도 되는 기능들을 밖으로 분리함으로써 객체 내의 주요한 로직들을 더 응집도있게 사용할 수 있는 것 같다.
  • 이 패턴은 행동이 클래스 계층 구조의 일부 클래스들에서만 의미가 있고 다른 클래스들에서는 의미가 없을 때 사용하세요.
    • 이 행동을 별도의 비지터 클래스로 추출한 후 관련 클래스들의 객체들을 수락하는 비지터 메서들만 구현하고 나머지는 비워둡니다.
    • 즉, 비지터 클래스로 빼낼 로직은 이 로직이 수행되는 객체외의 다른 객체에게 영향을 끼치지않는 로직으로 선정하라는 의미인 것 같다.

< 장점 & 단점 >

- 장점

  • OCP, 기존 코드를 고치지 않으면서 해당 클래스의 객체와 작동할 수 있는 새로운 동작을 추가가능.
  • SRP, 같은 행동의 여러 버전을 같은 클래스로 이동할 수 있음.
    • Json, XML 과 같은 여러 버전인데 같은 행동 → 데이터 형식으로의 처리
    • 이런 것들을 같은 클래스인 JsonVisitor, XMLVisitor로 이동시킬 수 있다.
  • 비지터 객체는 다양한 객체들과 작업하면서 유용한 정보를 축적할 수 있다. 이것은 객체 트리와 같은 복잡한 객체 구조를 순회하여 이 구조의 각 객체에 비지터 패턴을 적용하려는 경우에 유용할 수 있다.

- 단점

  • 클래스가 요소 계층구조에 추가되거나 제거될 때마다 모든 비지터를 업데이트해야함.
    • 새로운 Entity가 생기면 얘도 데이터 처리를 해야해서 visit 메서드를 전부 다 추가해야함.
  • 비지터들은 함께 작업해야 하는 요소들의 비공개 필드들 및 메서드들에 접근하기 위해 필요한 권한이 부족할 수 있음.
    • 기존엔 Entity 클래스 내부에서 처리해서 권한이 private까지 접근할 수 있었지만 다른 객체로 추출하다보니까 접근할 수 없는 권한이 생겼음.

다른 패턴과의 관계

  • 비지터 패턴은 커맨드 패턴의 강력한 버전으로 취급 가능. 비지터 패턴의 객체들은 다른 클래스들의 다양한 객체에 대한 작업을 실행 가능.
  • 비지터 패턴을 사용해서 컴포지트 패턴 트리 전체를 대상으로 작업을 수행할 수 있음.
  • 비지터 패턴이랑 반복자 패턴을 모두 사용해서 순회를 통해 모든 요소들에 대한 작업 수행 가능.


4. Reference

비지터 패턴
비지터(방문자) 패턴은 알고리즘들을 그들이 작동하는 객체들로부터 분리할 수 있도록 하는 행동 디자인 패턴입니다. 당신의 팀이 하나의 거대한 그래프로 구성된 지리 정보를 사용해 작동하는 앱을 개발하고 있다고 가정해 봅시다. 그래프의 각 노드는 도시와 같은 복잡한 객체를 나타낼 수 있지만 산업들, 관광 지역들 등의 더 세부적인 항목들도 나타낼 수 있습니다.
https://refactoring.guru/ko/design-patterns/visitor
자바로 작성된 비지터
비지터 는 기존 코드를 변경하지 않고 기존 클래스 계층구조에 새로운 행동들을 추가할 수 있도록 하는 행동 디자인 패턴입니다. 제 설명글 [비지터와 이중 디스패치]{비지터와 이중 디스패치}에서 왜 단순히 비지터들을 메서드 오버로딩으로 대체할 수 없는지 알아보세요. 복잡도: 인기도: 사용 사례들: 비지터는 복잡하고 적용 범위가 좁기 때문에 매우 일반적인 패턴이 아닙니다.
https://refactoring.guru/ko/design-patterns/visitor/java/example
GoF의 Design Pattern - 24. Visitor
https://www.youtube.com/watch?v=QC8Q5MWB-mQ

Uploaded by N2T