Visitor

Visitor 패턴이란?

Visitor 패턴은 객체 구조를 변경하지 않고, 새로운 기능을 객체에 추가할 수 있도록 해주는 행동 디자인 패턴입니다. 주로 객체 구조가 안정적이지만, 객체에 다양한 연산을 추가할 필요가 있을 때 유용합니다.

Visitor 패턴 구조

D2 Diagram

D2 Diagram

  • Client → Element
    • Client는 각 Element 객체에 대해 Accept(Visitor) 메서드를 호출하여 방문자를 전달합니다.
  • Element → Visitor
    • Element는 전달받은 Visitor 객체의 적절한 Visit(Element) 메서드를 실행합니다.
  • ConcreteElementA와 ConcreteElementB는 Element를 구현하며, 각자의 특수성을 가집니다.
  • ConcreteVisitor → Visitor
    • 호출된 Visit 메서드는 구체적인 방문자(ConcreteVisitor1, ConcreteVisitor2)의 구현체에서 실행됩니다.
    • ConcreteVisitor는 전달받은 Element 타입에 따라 고유한 작업을 수행합니다.

Visitor 패턴 적용

Visitor 패턴의 필요성

도서 관리 시스템에서 책의 다양한 형식(종이책, 전자책 등)에 대해 서로 다른 처리 로직이 필요할 때, 모든 책 객체에 일일이 새로운 기능을 추가하는 대신, Visitor 패턴을 사용하면 객체 구조를 변경하지 않고도 다양한 기능을 쉽게 추가할 수 있습니다.

잘못된 처리

기능을 추가할 때마다 객체의 클래스를 수정하는 방식은 코드 중복을 초래할 수 있으며, 클래스가 커지고 복잡해집니다.

public class Book
{
    public string Title { get; set; }
    
    public void ProcessAsPrintBook()
    {
        Console.WriteLine($"Processing printed book: {Title}");
    }
    
    public void ProcessAsEBook()
    {
        Console.WriteLine($"Processing e-book: {Title}");
    }
}

클래스 수정의 필요성

새로운 기능을 추가할 때마다 클래스를 수정해야 합니다. 이러한 방식은 유지보수성을 저하시킵니다.

확장성 부족

기존 클래스에 새로운 기능을 추가할 때, 코드의 중복과 수정이 필요합니다. 이는 개방-폐쇄 원칙을 위반할 수 있습니다.

Visitor 패턴 적용 예시

Visitor 패턴을 적용하면 객체 구조를 수정하지 않고도 새로운 기능을 쉽게 추가할 수 있습니다.

// Visitor 인터페이스
public interface IBookVisitor
{
    void Visit(PrintBook book);
    void Visit(EBook book);
}
// 구체적인 방문자: 세금 계산
public class TaxCalculator : IBookVisitor
{
    public void Visit(PrintBook book)
    {
        Console.WriteLine($"Calculating tax for printed book: {book.Title}");
    }
    public void Visit(EBook book)
    {
        Console.WriteLine($"Calculating tax for e-book: {book.Title}");
    }
}
// Book 클래스의 하위 클래스들
public interface IBook
{
    void Accept(IBookVisitor visitor);
}
public class PrintBook : IBook
{
    public string Title { get; set; }
    public void Accept(IBookVisitor visitor)
    {
        visitor.Visit(this);
    }
}
public class EBook : IBook
{
    public string Title { get; set; }
    public void Accept(IBookVisitor visitor)
    {
        visitor.Visit(this);
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        IBook printBook = new PrintBook { Title = "Design Patterns" };
        IBook eBook = new EBook { Title = "Clean Code" };
        IBookVisitor taxCalculator = new TaxCalculator();
        printBook.Accept(taxCalculator);  
        // Output: Calculating tax for printed book: Design Patterns
        eBook.Accept(taxCalculator);     
        // Output: Calculating tax for e-book: Clean Code
    }
}

Visitor 패턴 구성 요소

Visitor

방문자 인터페이스로, 객체 구조의 각 요소를 방문하여 처리하는 메서드를 정의합니다. 예제에서는 IBookVisitor 가 이에 해당합니다.

Concrete Visitor

구체적인 방문자 클래스로, 각 객체에 대해 구체적인 행동을 정의합니다. 예제에서는 TaxCalculator가 각 책에 대해 세금 계산을 처리합니다.

Element

객체 구조 내에서 Visitor를 받아들이는 인터페이스입니다. Accept() 메서드를 정의하여 방문자를 받아들이고, 방문자가 해당 객체에서 작업을 수행할 수 있게 합니다. 예제에서는 IBook 클래스가 역할을 담당합니다.

Concrete Element

구체적인 요소 클래스들로, 각 요소는 Visitor를 받아들여 자신의 구체적인 행동을 정의합니다. 예제에서는 PrintBook, EBook 클래스가 이에 해당합니다.

개선 사항

객체 구조의 분리

객체 구조를 수정하지 않고도 새로운 기능을 쉽게 추가할 수 있습니다.

기능 확장의 용이성

새로운 기능이 필요할 때 기존 코드의 수정 없이 새로운 Visitor 클래스를 추가하기만 하면 됩니다.

적용 시 유의 사항

구조의 복잡성

Visitor 패턴을 사용할 때 객체 구조가 복잡해질 수 있습니다. 각 클래스마다 Accept 메서드를 추가하고, Visitor 객체와의 의존성이 생깁니다.

상태 저장 및 관리

Visitor가 상태를 유지하거나 처리 중에 변경될 수 있습니다. 이를 위해 상태 관리를 신경 써야 합니다.

새로운 요소 추가의 어려움

새로운 객체가 객체 구조에 추가될 경우, 기존의 모든 Visitor 클래스가 수정되어야 하는 단점이 있습니다. 이로 인해 객체 구조가 고정되어야 하는 상황에서는 적용이 어려울 수 있습니다.

요소 인터페이스의 확장

새로운 요소가 추가될 때 기존 Visitor를 모두 수정해야 할 때, 인터페이스 설계를 유연하게 하여 확장에 대비할 수 있습니다.

객체지향 원칙과의 관계

Visitor와 다형성

Visitor 패턴은 다형성을 적극적으로 활용하는 패턴입니다. Visitor 패턴에서는 객체의 상태나 타입에 따라 서로 다른 Visitor 객체가 호출되며, 각기 다른 동작을 수행할 수 있습니다. 구체적인 Visitor 클래스들이 다형성을 통해 서로 다른 동작을 제공할 수 있습니다.

  • 다형성의 활용: IBookVisitor 인터페이스를 구현한 각 Visitor 클래스는 서로 다른 동작을 정의할 수 있으며, Book 객체는 다형성을 통해 다양한 Visitor 클래스를 받아들이고 그에 맞는 동작을 수행합니다.
  • 유연한 동작 제공: 객체는 동일한 메서드를 호출하더라도, 전달된 Visitor 객체에 따라 다른 동작을 수행할 수 있습니다. 예를 들어, PrintBook 객체에 대해 TaxCalculator가 적용될 때와 ShippingVisitor가 적용될 때의 동작은 다르지만, 동일한 Accept 메서드를 통해 처리됩니다.

Visitor와 단일 책임 원칙

Visitor 패턴은 객체의 본래 책임과 Visitor가 수행하는 행동을 분리하여, 객체는 자신의 데이터와 관련된 작업만 수행하고, 외부의 Visitor는 객체의 데이터를 이용해 부가적인 작업을 수행합니다.

  • 객체의 역할: 본래의 역할에 충실하며 자신의 상태나 데이터를 관리하는 데 집중합니다. 예를 들어, Book 클래스는 도서 정보(제목, 저자 등)를 관리하는 역할에 집중합니다.
  • Visitor의 역할: 객체의 상태를 바탕으로 부가적인 작업(세금 계산, 배송비 계산 등)을 처리합니다. Visitor는 객체와의 결합을 줄이고, 새로운 작업이 추가될 때마다 새로운 Visitor 클래스를 추가하는 방식으로 확장됩니다. 이러한 구조는 각각의 클래스가 하나의 책임에만 집중하도록 하여 단일 책임 원칙Single Responsibility Principle, SRP을 준수합니다.

Visitor와 개방_폐쇄 원칙

Visitor 패턴은 기존 코드를 수정하지 않고도 확장할 수 있도록 설계되어 있습니다. 새로운 기능(예: 세금 계산, 할인 처리)을 추가해야 할 경우, 객체를 수정할 필요 없이 새로운 Visitor 클래스를 추가하면 됩니다.

  • 확장: IBookVisitor 인터페이스를 구현하는 새로운 Visitor 클래스를 추가하여 기존 객체에 새로운 동작을 정의할 수 있습니다.
  • 기존 코드 수정 없음: 새로운 기능을 추가하더라도 기존 객체나 Visitor의 코드를 수정할 필요가 없으므로 개방-폐쇄 원칙Open/Closed Principle, OCP를 준수합니다. 예를 들어, 도서의 배송비 계산을 추가할 때, 기존 Book 클래스나 기존 Visitor 클래스들을 수정하지 않고, ShippingVisitor라는 새로운 클래스를 만들어 확장할 수 있습니다.

Visitor와 리스코프 치환 원칙

Visitor 패턴에서 구체적인 Visitor 클래스들은 IBookVisitor 인터페이스를 구현하고 있으며, 이는 다형성 원칙에 따라 상위 클래스나 인터페이스로 처리될 수 있습니다.

  • 상속 구조의 준수: IBookVisitor를 구현한 모든 Visitor는 IBookVisitor의 동작을 준수하므로, Visitor 인터페이스를 통해 어떤 Visitor 객체든 동등하게 처리할 수 있습니다.
  • 교체 가능성: IBookVisitor 인터페이스를 구현한 구체적인 Visitor 객체들은 언제든지 서로 교체 가능하며, 이는 리스코프 치환 원칙Liskov Substitution Principle, LSP의 교체 가능성을 보장합니다. 예를 들어, TaxCalculator 대신 ShippingVisitor를 사용해도 동일한 인터페이스로 처리되며, 시스템의 동작에 문제가 발생하지 않습니다.

Visitor와 의존 역전 원칙

구체적인 객체(Book, PrintBook, EBook)는 구체적인 Visitor 클래스에 의존하지 않고, 추상화된 인터페이스(IBookVisitor)에 의존합니다. 이를 통해 구체적인 구현에서 추상화를 통해 유연성을 확보하고, 구체적인 Visitor의 변경이 객체에 영향을 미치지 않도록 합니다.

  • 추상화된 인터페이스 의존: Book 객체는 구체적인 Visitor 클래스에 의존하지 않고, IBookVisitor 인터페이스에 의존하여 유연한 구조를 유지합니다.
  • 구체적인 구현으로부터 독립: 새로운 Visitor가 추가되더라도 객체의 구현에 변화가 없으므로 의존 역전 원칙Dependency Inversion Principle, DIP을 충족합니다. 이 원칙을 통해 Visitor 패턴은 구체적인 Visitor 클래스의 변경이 다른 시스템 구성 요소에 영향을 미치지 않도록 보장할 수 있습니다.

맺음말

Visitor 패턴은 객체 구조를 변경하지 않고도 새로운 기능을 쉽게 추가할 수 있는 유용한 패턴입니다. 다만, 새로운 요소가 추가될 때 Visitor 클래스를 수정해야 하는 단점이 있지만, 기능 확장이 자주 필요한 경우에 매우 적합합니다.

심화학습

Double Dispatch를 통한 기능 확장

Visitor 패턴은 흔히 Double Dispatch라는 개념을 통해 객체와 연산의 결합을 유연하게 만듭니다. Double Dispatch란 하나의 메서드 호출에 두 번의 동적 바인딩이 이루어지는 것을 의미하며, 이는 Visitor 패턴에서 Visitor와 Element가 서로를 호출하는 방식에서 나타납니다. 두 객체가 서로 상호작용할 때, 객체가 자신의 타입을 알리고, Visitor는 그 객체에 맞는 처리 방법을 선택할 수 있습니다. 이로 인해 Visitor 패턴은 다양한 객체 타입에 대해 적절한 행동을 처리하는 데 유용합니다.

public class ShippingVisitor : IBookVisitor
{
    public void Visit(PrintBook book)
    {
        Console.WriteLine($"Calculating shipping for printed book: {book.Title}");
    }
    public void Visit(EBook book)
    {
        Console.WriteLine($"No shipping needed for e-book: {book.Title}");
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        IBook printBook = new PrintBook { Title = "Design Patterns" };
        IBook eBook = new EBook { Title = "Clean Code" };
        IBookVisitor shippingVisitor = new ShippingVisitor();
        printBook.Accept(shippingVisitor);  
        // 출력: Calculating shipping for printed book: Design Patterns
        eBook.Accept(shippingVisitor);      
        // 출력: No shipping needed for e-book: Clean Code
    }
}

위 예제에서는 ShippingVisitor가 각 책의 종류에 따라 다른 처리를 수행합니다. Double Dispatch를 통해 책의 종류(PrintBook 또는 EBook)에 맞는 동작이 수행되며, Visitor는 각 객체의 타입을 기반으로 동작을 선택합니다.

Visitor와 Composite

Composite 패턴은 트리 구조를 사용하는 객체들을 관리하기 위한 패턴으로, 여러 객체를 하나의 객체처럼 다룰 수 있게 해줍니다. Visitor 패턴과 결합하면 객체 트리 구조를 순회하면서 각 객체에 대해 일관된 방식으로 작업을 수행할 수 있습니다. 이 두 패턴의 조합은 복잡한 트리 구조에서도 각 객체에 대해 다양한 작업을 수행하는 데 유리합니다. Composite 패턴으로 구성된 트리 구조에서 각 노드를 방문하여 다양한 연산을 수행할 수 있습니다. Visitor 패턴은 트리 구조의 각 요소(노드)와 상호작용하여 특정 작업(예: 계산, 출력, 저장 등)을 처리하는 데 적합합니다.

public class CompositeBook : IBook
{
    private List<IBook> _books = new List<IBook>();
    public void Add(IBook book)
    {
        _books.Add(book);
    }
    public void Accept(IBookVisitor visitor)
    {
        foreach (var book in _books)
        {
            book.Accept(visitor);
        }
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        CompositeBook library = new CompositeBook();
        library.Add(new PrintBook { Title = "Design Patterns" });
        library.Add(new EBook { Title = "Clean Code" });
        IBookVisitor visitor = new TaxCalculator();
        library.Accept(visitor);  
        // 여러 책에 대해 세금 계산 처리
    }
}

이 예제에서는 CompositeBook이 여러 책을 관리하며, 각 책에 대해 Visitor 패턴을 사용해 다양한 작업을 수행할 수 있습니다. Composite 패턴을 사용하면 트리 구조의 노드에 대해 작업을 쉽게 수행할 수 있고, Visitor 패턴을 통해 그 작업을 확장할 수 있습니다.

Visitor와 Decorator

Decorator 패턴은 객체에 동적으로 기능을 추가하는 데 사용되는 구조 패턴입니다. Visitor 패턴과 결합하면 Decorator를 통해 객체에 동적으로 추가된 기능을 Visitor를 통해 처리할 수 있습니다. 이 방식은 객체의 동작을 동적으로 변경하거나 확장하는 데 매우 유용합니다.

public class DiscountedBook : IBook
{
    private IBook _originalBook;
    private double _discount;
    public DiscountedBook(IBook originalBook, double discount)
    {
        _originalBook = originalBook;
        _discount = discount;
    }
    public void Accept(IBookVisitor visitor)
    {
        _originalBook.Accept(visitor);
        Console.WriteLine($"Applying discount: {_discount * 100}%");
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        IBook printBook = new PrintBook { Title = "Refactoring" };
        IBook discountedBook = new DiscountedBook(printBook, 0.10); // 10% 할인
        IBookVisitor visitor = new TaxCalculator();
        discountedBook.Accept(visitor);  // 책에 대한 세금 계산 후, 할인을 적용
    }
}

DiscountedBook 클래스는 기존의 IBook 객체에 할인 기능을 추가한 Decorator 클래스입니다. 이 객체에 대해 Visitor가 세금 계산과 같은 작업을 수행하면서 할인율도 적용됩니다. 이로써 새로운 기능을 쉽게 추가하고, Visitor 패턴을 통해 그 기능을 처리할 수 있습니다.

Visitor와 Chain of Responsibility

Chain of Responsibility 패턴은 요청을 처리할 수 있는 여러 객체를 연쇄적으로 연결하여, 그중 하나가 요청을 처리하도록 하는 패턴입니다. Visitor 패턴과 결합하면 Visitor가 여러 객체를 순차적으로 방문하면서 각 객체에 대한 처리를 연쇄적으로 진행할 수 있습니다.

public class ProcessingChainVisitor : IBookVisitor
{
    public void Visit(PrintBook book)
    {
        Console.WriteLine($"Processing printed book in chain: {book.Title}");
    }
    public void Visit(EBook book)
    {
        Console.WriteLine($"Processing e-book in chain: {book.Title}");
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        IBook printBook = new PrintBook { Title = "Effective Java" };
        IBook eBook = new EBook { Title = "Java Concurrency in Practice" };
        IBookVisitor chainVisitor = new ProcessingChainVisitor();
        printBook.Accept(chainVisitor);  // 첫 번째 객체가 처리
        eBook.Accept(chainVisitor);      // 두 번째 객체가 처리
    }
}

ProcessingChainVisitor는 각각의 객체를 방문하면서 처리 작업을 진행합니다. 각 객체는 자신이 처리할 작업을 수행하며, 필요하면 다음 객체로 요청을 넘길 수 있습니다. 이 조합은 연쇄적으로 작업을 처리할 때 유용합니다.

Visitor와 State

상태 패턴State Pattern과 Visitor 패턴을 결합하면 객체의 상태에 따라 Visitor가 다른 작업을 수행할 수 있습니다. 객체의 상태 변화에 따라 Visitor의 처리가 달라지며, 이는 동적으로 상태를 관리할 때 유용합니다.

public class ReservedBookState : IBookState
{
    public void Accept(IBookVisitor visitor)
    {
        visitor.Visit(this);
        Console.WriteLine("Handling reserved book state.");
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        Book book = new Book();
        book.SetState(new ReservedBookState());
        IBookVisitor visitor = new TaxCalculator();
        book.Accept(visitor);  // 상태에 따라 Visitor가 동작
    }
}

ReservedBookState는 책이 예약된 상태일 때 Visitor가 처리해야 할 작업을 정의합니다. Visitor는 각 상태에 맞게 동작을 선택하며, 상태가 변경되면 Visitor의 처리 방식도 유연하게 변합니다.