본문 바로가기
컴퓨터 과학/디자인패턴

[디자인패턴] 행위 패턴(12) : 방문자(Visitor)

by webcodur 2024. 3. 25.
728x90

목차

     

    방문자(Visitor)

    방문자 패턴(Visitor Pattern)은 객체의 구조를 변경하지 않고도 객체에 새로운 연산을 쉽게 추가할 수 있도록 하는 디자인 패턴이다. 이 패턴은 주로 데이터 구조 내의 각 요소에 대해 수행될 연산을 객체 구조에서 분리하여, 새로운 연산을 추가할 때 기존 구조를 수정하지 않아도 되게 만든다. "방문자"라는 용어는 패턴의 구성 요소 중 하나인 'Visitor' 객체가 데이터 구조의 각 요소를 "방문"하여 특정 작업을 수행한다는 개념에서 유래한다.

     

    패턴 미적용 예시

    게임 개발 시나리오에서 플레이어가 아이템을 습득하거나 특정 지역에 진입하는 등의 이벤트를 처리하는 코드를 먼저 살펴보자. 방문자 패턴을 적용하지 않고, 이벤트를 처리하는 간단한 구조를 구현해본다.

    using System;
    
    public class Item {
        public string Name { get; set; }
        public Item(string name) {
            Name = name;
        }
    }
    
    public class Area {
        public string Name { get; set; }
        public Area(string name) {
            Name = name;
        }
    }
    
    public class GameEvent {
        public void HandleItemAcquisition(Item item) {
            Console.WriteLine($"아이템을 습득하였습니다: {item.Name}");
        }
    
        public void HandleAreaEntry(Area area) {
            Console.WriteLine($"지역에 진입하였습니다: {area.Name}");
        }
    }
    
    class Program {
        static void Main(string[] args) {
            var item = new Item("검");
            var area = new Area("숲");
    
            var gameEvent = new GameEvent();
            gameEvent.HandleItemAcquisition(item);
            gameEvent.HandleAreaEntry(area);
        }
    }
    

    이 예시에서는 GameEvent 클래스가 아이템 습득과 지역 진입 이벤트를 모두 처리한다. 새로운 이벤트 유형을 추가하려면 GameEvent 클래스를 수정해야 하므로, 이벤트 처리 로직의 확장성과 유연성이 떨어진다는 문제가 있다.

     

    패턴 적용 예시

    이제 동일한 시나리오에 방문자 패턴을 적용해보자. 이벤트 처리 로직을 개선하여, 새로운 이벤트 유형을 추가할 때 기존 코드를 변경하지 않고도 확장할 수 있게 만든다.

    using System;
    
    public interface IGameEventVisitor {
        void Visit(ItemAcquisitionEvent itemEvent);
        void Visit(AreaEntryEvent areaEvent);
    }
    
    public interface IGameEvent {
        void Accept(IGameEventVisitor visitor);
    }
    
    public class ItemAcquisitionEvent : IGameEvent {
        public Item Item { get; private set; }
        public ItemAcquisitionEvent(Item item) {
            Item = item;
        }
    
        public void Accept(IGameEventVisitor visitor) {
            visitor.Visit(this);
        }
    }
    
    public class AreaEntryEvent : IGameEvent {
        public Area Area { get; private set; }
        public AreaEntryEvent(Area area) {
            Area = area;
        }
    
        public void Accept(IGameEventVisitor visitor) {
            visitor.Visit(this);
        }
    }
    
    public class EventLogger : IGameEventVisitor {
        public void Visit(ItemAcquisitionEvent itemEvent) {
            Console.WriteLine($"아이템을 습득하였습니다: {itemEvent.Item.Name}");
        }
    
        public void Visit(AreaEntryEvent areaEvent) {
            Console.WriteLine($"지역에 진입하였습니다: {areaEvent.Area.Name}");
        }
    }
    
    class Program {
        static void Main(string[] args) {
            // Item과 Area 클래스는 패턴 미적용 예시 참고
            IGameEvent itemEvent = new ItemAcquisitionEvent(new Item("검"));
            IGameEvent areaEvent = new AreaEntryEvent(new Area("숲"));
    
            // Event 외부에서 미리 visitor 지정 (Accept 전에 수행)
            IGameEventVisitor visitor = new EventLogger();
    
            // Event는 visitor가 Event에 따른 작업을 수행하도록 작업 승인
            itemEvent.Accept(visitor); // 아이템을 습득하였습니다: 검
            areaEvent.Accept(visitor); // 지역에 진입하였습니다: 숲
        }
    }
    

     

    이 예시에서는 방문자 패턴을 적용하여 IGameEvent 인터페이스를 구현하는 각 이벤트 클래스(ItemAcquisitionEvent, AreaEntryEvent)가 있으며, 이들은 Accept 메서드를 통해 방문자(IGameEventVisitor 구현체)를 받아들인다. 방문자는 Visit 메서드를 통해 해당 이벤트의 구체적인 처리 로직을 수행한다. 이 경우, **EventLogger**가 방문자의 역할을 하여, 아이템 습득 이벤트와 지역 진입 이벤트를 로깅한다.

     

    방문자 패턴 적용의 장점:

    • 확장성: 새로운 이벤트 유형을 추가하려면 IGameEvent 인터페이스를 구현하는 새로운 이벤트 클래스를 만들고, IGameEventVisitor 인터페이스에 새로운 Visit 메서드를 추가하기만 하면 된다. 기존 코드를 변경할 필요가 없어, 시스템의 확장성이 크게 향상된다.
    • 분리와 조직화: 이벤트 처리 로직을 이벤트 데이터 구조에서 분리함으로써, 로직과 데이터를 깔끔하게 조직할 수 있다. 이벤트 처리 코드를 중앙에서 관리할 수 있어 유지보수가 용이하다.
    • 유연성: 다양한 이벤트에 대해 다른 방식의 처리를 유연하게 적용할 수 있다. 예를 들어, 로깅 외에도 이벤트 데이터를 기반으로 통계를 내거나, 게임의 상태를 변경하는 등 다양한 방문자를 구현할 수 있다.

     

    이해를 돕는 다른 실세계 예시

    방문자 패턴을 실세계 예시로 비교하자면, 공항의 보안 검사 절차를 생각해볼 수 있다. 공항에는 여러 종류의 승객들이 있고, 각 승객은 자신만의 특성(목적지, 짐의 양, 비행 클래스 등)을 가지고 있다. 이제 여기서 보안 검사원을 "방문자"로 생각할 수 있다.

     

    • 승객들(객체): 공항을 이용하는 다양한 승객들은 IGameEvent 인터페이스를 구현하는 객체들과 비슷하다. 각 승객은 검사를 받아야 하는 대상이며, 검사를 받는 방식(즉, 실제 수행되는 작업)은 승객의 특성에 따라 다를 수 있다.
    • 보안 검사원(방문자): 보안 검사원은 IGameEventVisitor 인터페이스를 구현하는 "방문자"와 유사하다. 검사원은 승객(객체)을 방문하여 특정 작업(보안 검사)을 수행한다.
    • 검사 절차(Accept 메소드): 승객이 보안 검사를 받는 과정은 Accept 메소드 호출과 비슷하다. 승객은 검사원(방문자)을 "수용"하며, 검사원은 승객의 특성에 맞는 검사(작업)를 수행한다.
    • 검사 수행(Visit 메소드): 검사원이 승객에 대해 실제로 수행하는 보안 검사 절차는 Visit 메소드의 실행과 유사하다. 검사원은 승객의 종류(예: 국내선 승객, 국제선 승객, 우선 승객 등)에 따라 다른 절차를 따를 수 있다.

    이 예시에서, 보안 검사원(방문자)은 공항에 있는 다양한 승객들(객체)에 대해 특정 작업(보안 검사)을 수행한다. 방문자 패턴은 승객(객체)이 검사원(방문자)에 의해 "방문"받는 상황과 유사하며, 각 승객(객체)의 특성에 맞는 적절한 검사(작업)를 수행하도록 한다. 이러한 방식으로, 공항 보안 검사 시스템은 다양한 승객 유형을 유연하게 처리할 수 있으며, 새로운 승객 유형이나 검사 기준이 추가되어도 기존 시스템을 크게 변경하지 않고도 쉽게 적응할 수 있다.

     

    결론

    방문자 패턴을 적용함으로써, 게임 개발과 같이 복잡한 시스템에서 발생할 수 있는 다양한 이벤트를 효과적으로 처리할 수 있다. 이 패턴은 이벤트 처리 로직의 확장성, 유연성을 크게 향상시키며, 시스템의 다양한 부분을 깔끔하게 분리하고 조직화할 수 있도록 돕는다.