Java/Design Pattern

[디자인 패턴] 행동 패턴 - 방문자 패턴(Visitor Pattern)

민돌v 2023. 5. 5. 00:42
728x90
(인프런) 코딩으로 학습하는 GoF의 디자인 패턴 - 백기선, 강의를 보고 정리한 글입니다.
코드는 GitHub 에 있습니다

 

#1. 객체 생성 관련 패턴

#2. 구조 관련 패턴

 

#3. 행동 관련 패턴

 

 


✔️ 방문자 패턴(Visitor Pattern)

비지터 패턴은, 기존 코드를 건드리지 않고 새로운 코드를 추가하는 방법을 제안하는 패턴
  • 방문자 패턴은 비지니스 로직을 객체 구조에서 분리시키는 디자인 패턴입니다.
  • 비슷한 종류의 객체들을 가진 그룹에서 작업을 수행해야 할 때 주로 사용됩니다.
  • 📌 더블 디스패치 (Doublie Dispatch)를 활용합니다. https://www.baeldung.com/ddd-double-dispatch
    • 디스패치란 (Dispatch 1.(특별한 목적을 위해)보내다  2.(메세지를) 보내다 ) 
      • 자바는 객체지향 프로그래밍 언어로써 객체들 간의 메세지 전송을 기반으로 문제를 해결합니다.
      • 메세지  전송이라는 표현은 결국 메서드를 호출하는 것인데 그것을 Dispatch라고 합니다.
    • 자바는 하위타입으로의 묵시적 형변환을 지원하지 않는, 싱글 디스패치 (Single Dispatch) 언어입니다.  - 따라서 런타임시에 부모 객체의 구현체로 어떤 자식 클래스가 들어오는지 확인하여 서로 다른 매서드를 호출해주는 동적 디스패치를 지원하지 않습니다.
    • 이러한 언어의 한계적 특성으로 인해 인자에 동적 디스패치를 활용하지못하기때문에, 이를 위해 한번 더 다형성을 이용함으로서 호출객체 동적 디스패치를 이용하는 기법이 더블 디스패치 입니다.

 

 

방문자 패턴 UMR 다이어그램

  • Client : 명령을 보냅니다.
  • Visitor : 명령을 수행하기 위해 필요한 메소드를 정의하는 인터페이스입니다.
  • ConcreteVisitor : 명령을 수행하는 메소드를 구현합니다.
  • Element : Visit를 사용할 수 있는지 확인하는 accept 메소드를 정의하는 인터페이스입니다.
  • ConcreteElement : Visitable에서 정의된 accept 메소드를 구현하며 Visitor객체는 이 객체를 통하여 명령이 전달됩니다.

✔️  방문자 패턴 예시

  • 인터페이스(2)를 인자로 받는 인터페이스(1)가 있다고 가정하자
  • 그후, 1차 인터페이스(1)의 구현체에서 인자로 받은 2차 인터페이스 (2) 의 구현체 타입에따라 다른 행위를 해야하는 요구조건이라면
  • 비지터 패턴을 적용하지 않고 "분기 처리"로 해당 매개변수로 들어온 구현체의 클래스를 판단한다면 아래와 같은 코드가 생성됩니다.

 

방문자 패턴 적용 전

  • Shape 인터페이스를 상속받는 "도형" 구현체에게 들어오는 Device 인터페이스의 구현체에따라 각각 모든 조건에 따른 분기처리를 진행해야합니다.
  • 이렇게하면, Shape 혹은 Device의 구현체가 늘어나거나 특정 상황에서의 행위가 달라지면 무수히 많은 코드들을 수정해야하는 매우 강한 결합상태입니다.
  • 방문자 패턴의 장점은   Double Dispatcher 이므로, 굳이 표현한다면  Shape 구현체로 이동되는 1차 Dispatch 가 일어납니다.
public interface Shape {

    void printTo(Device device);
}

//구현체 1
public class Rectangle implements Shape {

    //디바이스의 구현체가 추가되더라도 여기 코드가 변경됨
    @Override
    public void printTo(Device device) {
        if (device instanceof Phone) {
            System.out.println("print Rectangle to Phone");
        }
        if (device instanceof Watch) {
            System.out.println("print Rectangle to Watch");
        }
    }
}

//구현체 2
public class Triangle implements Shape{

    @Override
    public void printTo(Device device) {
        if (device instanceof Phone) {
            System.out.println("print Triangle to Phone");
        }

        if (device instanceof Watch) {
            System.out.println("print Triangle to Watch");
        }
    }
}

 

public interface Device {

}

public class Phone implements Device{

}

public class Watch implements Device{

}

 

public class Client {

    public static void main(String[] args) {
        Shape rectangle = new Rectangle();
        Shape triangle = new Triangle();

        Device phone = new Phone();
        Device watch = new Watch();
        
        rectangle.printTo(watch);
        rectangle.printTo(phone);

        triangle.printTo(watch);
        triangle.printTo(phone);
    }
}

 

👇

 

방문자 패턴 적용 후

  • 이제 비지터 패턴을 이용하여 해당 코드의 의존성을 제거해 보도록 하겠습니다.
  • 방문자 패턴의 핵심은 더블 디스패치. 기존의 Shape 구현체에서 일어나던 비지니스 로직을 Device 에게 직접 처리하도록 수정한것입니다.

  1. Client에서 accept() 메소드를 찾기위해 1차 Dispatch (정적 디스패치) 가 일어나고 
  2. print() 객체를 호출하기 위해 위해 메서드를 호출하는 인자가 구현체가 없는 인터페이스 타입인 경우 런타임시에 객체를 찾아 알맞는 메서드를 호출하는 2차 Dispatch (동적 디스패치) 가 일어납니다.
    • (정적 1 + 동적 1 = 2번의 디스패치가 일어남) 
public interface Shape {
    //비지터로 디스페치 하기위한 기능
    void accept(Device device);
}

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

public class Circle implements Shape {

    @Override
    public void accept(Device device) {
        device.print(this);
    }
}

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

 

public interface Device {

    void print(Circle circle);

    void print(Triangle triangle);

    void print(Rectangle rectangle);
    
}
//필요한 기능이 생기면 변경하지 않고 추가하면 됨
public class Watch implements Device {

    @Override
    public void print(Circle circle) {
        System.out.println("print Circle to Watch");
    }

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

    @Override
    public void print(Rectangle rectangle) { System.out.println("print Rectangle to Watch");}
}
public class Phone implements Device {

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

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

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

 

호출하는 클라이언트는 똑같습니다.

public class Client {

    public static void main(String[] args) {
        Shape rectangle = new Rectangle();
        Shape circle = new Circle();
        Shape triangle = new Triangle();

        Device phone = new Phone();
        Device watch = new Watch();

        //상위 타입인 device 가 지원이 되지 않음
        rectangle.accept(phone);
        rectangle.accept(watch);

        circle.accept(phone);
        circle.accept(watch);

        triangle.accept(phone);
        triangle.accept(watch);
    }
}

 

 


 

✔️ 방문자 패턴 장점, 단점

• 장점

  1. 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있다. (1차 디스패치기능은 추가해야함)
  2. 추가 기능을 한 곳에 모아둘 수 있다.

 

• 단점

  1. 새로운 Element 를 추가하거나 제거할 때 모든 Visitor 코드를 변경해야 한다. 
  2. visit 메소드의 return 타입을 각각 파악하고 있어야 한다.

 

 

 

 

 

 

 

 


참고

 

반응형