디자인 패턴

비지터 패턴 (Visitor Pattern)

개발정리 2022. 8. 13. 23:10

비지터 패턴


기존 코드를 변경하지 않고 새로운 기능을 추가하는 방법

  • 더블 디스패치 (Double Dispatch) 를 활용할 수 있다.

 

 

아래와 같은 경우 해당 패턴을 적용해 볼 수 있다.

  • 특정 클래스에 SRP 를 지키고 싶어 더는 책임을 늘리고 싶지 않을 때 
  • 버그가 발생할 수 있으니 더 이상 코드를 작성하고 싶지 않을 때
  • 해당 코드는 다른 곳에서도 사용될 수 있을거 같다 라는 생각이 들 때 

 

 

 

 

비지터 패턴 적용 전


특정 디바이스에 도형을 그려야 한다. 디바이스 클래스를 각 구체적인 디바이스 (ex. phone, watch) 들이 상속하고 Shape 인터페이스를 도형들이 implements 한다. 

 

 

Device 클래스와 그것을 구현한 각 디바이스들은 모두 비어 있는 클래스이다.

public class Device {
}
public class Phone extends Device{
}
public class Watch extends Device{
}

 

 

Shape 인터페이스

public interface Shape {
    void printTo(Device device);
}

각 도형들은 printTo 의 인자로 받는 Device 의 종류에 따라 다른 방법으로 도형을 그린다고 가정하자. 

 

 

Triangle 클래스

public class Triangle implements Shape {
    @Override
    public void printTo(Device device) {
        if (device instanceof Phone) {
            System.out.println("print Triangle to Phone");
        } else if (device instanceof Watch) {
            System.out.println("print Triangle to Watch");
        } 
        // Device 가 더 추가된다면?
    }
}

 

 

Circle 클래스

public class Circle implements Shape {
    @Override
    public void printTo(Device device) {
        if (device instanceof Phone) {
            System.out.println("print Circle to phone");
        } else if (device instanceof Watch) {
            System.out.println("print Circle to watch");
        }
    }
}

 

 

Rectangle 클래스

public class Rectangle implements Shape {
    @Override
    public void printTo(Device device) {
        if (device instanceof Phone) {
            System.out.println("print Rectangle to phone");
        } else if (device instanceof Watch) {
            System.out.println("print Rectangle to watch");
        }
    }
}

 

 

Client 클래스

public class Client {
    public static void main(String[] args) {
        Shape rectangle = new Rectangle();
        Device device = new Phone();
        rectangle.printTo(device);
    }
}

 

 

문제점

만약 다른 디바이스가 추가된다면 기존 모든 클래스 (Circle, Triangle, Rectangle) 에 if 문을 추가해주어야 한다. 이는 OCP 위반이다. 

Shape 구현체들이 직접 도형을 그려주는 것이 맞는지 의문도 생긴다. 비지터 패턴을 적용하여 해당 문제를 해결해보자.

 

참고로 위의 예는 if 문이 없이도 가능하지만 최대한 간단하고 직관적인 이해를 위해 if 문으로 분기처리 하였다.

 

 

 

비지터 패턴 적용 후


 

 

Shape 의 그림을 그리는 로직을 모두 device 로 옮길 것이다. 

Shape 에는 accept 메서드 하나만 만들어준다.

// Element interface
public interface Shape {
    void accept(Device device);
}

 

 

Shape 구현체들은 Device 로 옮겨진 print 메서드를 호출한다.

// Element
public class Triangle implements Shape {
    @Override
    public void accept(Device device) {
        device.print(this);
    }
}
// Element
public class Circle implements Shape {
    @Override
    public void accept(Device device) {
        device.print(this);
    }
}
// Element
public class Rectangle implements Shape {
    @Override
    public void accept(Device device) {
        device.print(this);
    }
}

이 때 구체 클래스 타입이 아닌 인터페이스 Device 타입으로 들어온 device 의 print 메서드를 실행하기 위해 디스패치가 발생한다.

 

 

이제 Device 를 보자.

// visitor
public interface Device {
    void print(Circle circle);

    void print(Rectangle rectangle);

    void print(Triangle triangle);
}

Device 는 각 도형들을 그리는 모든 메서드들을 가지고 있다. (오버로딩 메소드들)

 

 

Phone 과 Watch 는 ConcreteVisitor 에 해당된다.

public class Phone implements Device {
    
    @Override
    public void print(Circle circle) {
        System.out.println("Print Circle to Phone");
    }

    @Override
    public void print(Rectangle rectangle) {
        System.out.println("Print Rectangle to Phone");

    }

    @Override
    public void print(Triangle triangle) {
        System.out.println("Print Triangle to Phone");
    }
}
public class Watch implements Device {
    @Override
    public void print(Circle circle) {
        System.out.println("Print Circle to Watch");
    }

    @Override
    public void print(Rectangle rectangle) {
        System.out.println("Print Rectangle to Watch");
    }

    @Override
    public void print(Triangle triangle) {
        System.out.println("Print Triangle to Watch");
    }
}

 

 

Client

public class Client {
    public static void main(String[] args) {
        Shape rectangle = new Rectangle();
        Device device = new Pad();
        rectangle.accept(device);
    }
}

클라이언트 코드를 실행하면 총 2번의 디스패치가 발생한다.

  • Shape rectangle 의 타입이 Shape 이므로 어떤 구체 클래스의 accept 메서드를 실행해야 할지에 대한 디스패치 발생
  • 위에서 언급했듯이 accept 메소드 안에서 Device 타입으로 들어온 device 의 print 메서드를 실행하기 위해 디스패치 발생

 

 

이렇게 되면 새로운 Device 가 추가되더라도 기존의 코드를 변경하지 않고 추가할 수 있다. 

Ipad 를 추가해보자.

public class Pad implements Device {
    @Override
    public void print(Circle circle) {
        System.out.println("Print Circle to Pad");
    }

    @Override
    public void print(Rectangle rectangle) {
        System.out.println("Print Rectangle to Pad");
    }

    @Override
    public void print(Triangle triangle) {
        System.out.println("Print Triangle to Pad");
    }
}

 

 

새로운 Device 인 Ipad 를 추가하였는데 기존의 코드는 전혀 변경되지 않았다.

 

 

 

장점과 단점


장점

  • 기존 코드를 수정하지 않고 새로운 코드를 추가할 수 있다.
  • 추가 기능을 한곳에 모아둘 수 있다.

 

단점

  • 복잡하다.
  • 새로운 Element 를 추가하거나 제거할 때 모든 Visitor 코드를 수정해야 한다.

 

 

 

실무에선 어떻게 쓰이나?


  • 자바의 File Visitor, SimpleFile Visitor, AnnotationValue Visitor, ElementVisitor
  • 스프링의 BeanDefinitionVisitor

 

 

 

참고


 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com