본문 바로가기
Develop Story/DesignPattern

Visitor Pattern

by 박은유 2024. 4. 1.
반응형

Visitor 패턴

 : 데이터 구조와 처리를 분리합니다.

 

 여기 물건을 판매하는 가게가 있습니다. 이 가게에는 여러 종류의 상품이 있고, 각 상품에는 정보가 있습니다.

 

class Book {
    private double price;

    public Book(double price) {
        this.price = price;
    }

    public double getPrice() {
        return price;
    }
}

class Fruit {
    private double weight;
    private double pricePerKg;

    public Fruit(double weight, double pricePerKg) {
        this.weight = weight;
        this.pricePerKg = pricePerKg;
    }

    public double getPrice() {
        return weight * pricePerKg;
    }
}

 

위처럼 책과 과일을 판매중이라고 합시다. 이제 구매자가 책과 과일을 원하는 만큼 들고와서 계산을 요청합니다. 그럴 때 가격의 총합을 구하는 방식은 다음과 같이 구현 할 수 있습니다.

 

1. 각각의 물건들의 가격을 일일히 더한다.

 

public class Main {
    public static void main(String[] args) {
        Book bookA = new Book(20);
        Book bookB = new Book(30);
        Fruit apple = new apple(2, 3.5);
        Fruit melon = new apple(3, 5);
		
        int totalPrice = 0;
        
        totalPrice = bookA.getPrice() + bookB.getPrice() + apple.getPrice() + melon.getPrice();

        System.out.println("Total Price: $" + totalPrice);
    }
}

 

 위의 방식처럼 각 객체의 get메서드로 가격을 가져와 일일히 더해서 계산합니다. 하지만 중복코드가 많고, 객체가 늘어나면 그에따른 코드 수정또한 불가피합니다.

 

이럴 때 Visitor Pattern이 등장합니다.

 

 구현 방법은 다음과 같습니다.

 

1)데이터 구조 안을 돌아다니는 주체인 '방문자'를 나타내는 클래스를 준비하고 그 클래스에 처리를 맡깁니다.

2)데이터 구조 쪽에서는 '방문자'를 받아 주면 됩니다.

3)새로운 처리를 추가하고 싶을 때는 새로운 '방문자'를 만들면 됩니다.

 

 

1)데이터 구조 안을 돌아다니는 주체인 '방문자'를 나타내는 클래스를 준비하고 그 클래스에 처리를 맡깁니다.

 

먼저 각각의 객체를 방문하며 처리를 도와줄 방문자 클래스가 필요합니다. 

// Visitor interface
interface Visitor {
    void visit(Book book);
    void visit(Fruit fruit);
}

 

Visitor 인터페이스는 각각의 객체를 방문 할 visit 메소드를 파라미터만 다르게 여러개 가지고 있습니다. 이를 '오버로딩'이라고 합니다. visit 메소드는 객체를 방문할 때 각각의 객체가 호출하는 메소드입니다. 이제 실제 방문자 역할을 할 클래스를 구현합니다.

 

// ConcreteVisitor class
class ShoppingCartVisitor implements Visitor {
    private double totalPrice = 0;

    @Override
    public void visit(Book book) {
        totalPrice += book.getPrice();
    }

    @Override
    public void visit(Fruit fruit) {
        totalPrice += fruit.getPrice();
    }

    public double getTotalPrice() {
        return totalPrice;
    }
}

 

ShoppingCartVisitor 클래스는 총 가격을 변수로 선언해두고 각각의 객체를 방문해서 가격을 더한 다음, getTotalPrice()로 합산된 가격을 반환해줍니다. 말 그대로 쇼핑카트의 역할을 하는 클래스입니다.

 

2)데이터 구조 쪽에서는 '방문자'를 받아 주면 됩니다.

이제 각 객체들이 방문자를 받아 줄 수 있게 해주면 됩니다. 

// Element interface
interface Product {
    void accept(Visitor visitor);
}

 

Product 인터페이스는 방문자를 받아들이는 인터페이스입니다. 이제 이 인터페이스를 각각의 상품들이 구현하도록 합니다.

 

// ConcreteElement class - Book
class Book implements Product {
    private double price;

    public Book(double price) {
        this.price = price;
    }

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

    public double getPrice() {
        return price;
    }
}

// ConcreteElement class - Fruit
class Fruit implements Product {
    private double weight;
    private double pricePerKg;

    public Fruit(double weight, double pricePerKg) {
        this.weight = weight;
        this.pricePerKg = pricePerKg;
    }

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

    public double getPrice() {
        return weight * pricePerKg;
    }
}

 

이렇게 Product Interface를 구현하면 준비는 끝이 납니다.

 

이제 다시 계산을 해보겠습니다.

 

// Client code
public class Main {
    public static void main(String[] args) {
        List<Product> products = new ArrayList<>();
        products.add(new Book(20));
        products.add(new Book(30));
        products.add(new Fruit(2, 3.5));
        products.add(new Fruit(3, 5));

        ShoppingCartVisitor visitor = new ShoppingCartVisitor();

        for (Product product : products) {
            product.accept(visitor);
        }

        System.out.println("Total Price: $" + visitor.getTotalPrice());
    }
}

 

이제 가격의 총합을 구하는(처리)역할을 visitor에게 위임했습니다. 훨씬 깔끔해졌죠?

 

그때 정부에서 앞으로 책과 과일에 세금이 붙는다는 뉴스가 나옵니다. 10%가 붙는다고 하면 totalPrice에 10%를 붙이면 됩니다. 여기까지는 괜찮지만 만약 책과 과일에 각각 다른 세금이 붙는다면 어떻게 할까요? 또 외국인에게는 기존처럼 세금이 붙지않은 가격으로 판매를 해야된다면요?

 

책에는 10%, 과일에는 20%의 세금이 붙는다고 하면 각각의 가격에 세율을 곱해줘야 합니다. 그게 싫다면 객체의 get메소드에 세율을 붙이는 방법도 있습니다. 하지만 이건 좋은 방법이 아닙니다. 기존 클래스의 변경이 일어나면 get메소드를 사용하고 있는 곳에 모두 영향이 가게되는, 즉 OCP(Open-Closed Principle)원칙을 위배하기 때문입니다. 또, 기존의 ShoppingCartVisitor는 외국인대상으로 가격을 계산하기 위해서 그대로 둬야합니다.

 

이때 Visitor Pattern의 진면목이 나옵니다.

 

3)새로운 처리를 추가하고 싶을 때는 새로운 '방문자'를 만들면 됩니다.

 

// ConcreteVisitor class
class TaxPriceVisitor implements Visitor {
    private double totalPrice = 0;

    @Override
    public void visit(Book book) {
        totalPrice += 1.1*book.getPrice();
    }

    @Override
    public void visit(Fruit fruit) {
        totalPrice += 1.2*fruit.getPrice();
    }

    public double getTotalPrice() {
        return totalPrice;
    }
}

 

이렇게 TaxPriceVisitor라는 새로운 방문자를 추가하면 됩니다. 이처럼 데이터 구조와 처리를 분리해둔다면 처리과정이 추가되도 '방문자'만 추가하면 됩니다.

 

하지만 Visitor패턴의 단점도 존재합니다.

 

단점

1) 종속성이 강하다.

 : ConcreteElement 클래스와 Visitor 클래스 간의 강한 결합이 도입됩니다. 새로운 ConcreteElement 클래스(상품)를 추가하거나 Visitor 클래스의 계층 구조를 변경하면 기존 코드에 영향을 미칠 수 있습니다.

 

2) 복잡성이 증가할 수 있다.

 : Visitor 패턴은 일반적으로 추가적인 클래스 및 인터페이스를 도입하여 코드의 복잡성을 증가시킵니다. 이는 간단한 기능을 구현하는데 있어서는 오버헤드가 발생할 수 있습니다. 따라서 간단한 구조에서는 오히려 불필요한 복잡성을 도입할 수 있습니다.

 

정리

Visitor 패턴은 객체 구조를 돌아다니며 각각의 객체에 대해 다양한 처리를 수행하고자 할 때 유용한 디자인 패턴으로, 객체 구조와 처리를 분리하여 확장성을 높이고 유지보수를 용이하게 합니다.

 

반응형

'Develop Story > DesignPattern' 카테고리의 다른 글

Memento Pattern  (0) 2024.04.15
Chain of Responsibility Pattern  (0) 2024.04.08
Bridge Pattern  (0) 2024.03.25