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

[디자인패턴] 행위 패턴(1) : 책임 연쇄(Chain of Responsibility)

by webcodur 2024. 3. 25.
728x90

목차

     

     

    책임 연쇄(Chain of Responsibility)

    책임 연쇄 디자인 패턴은 행동 패턴의 하나로, 요청을 처리할 수 있는 기회를 여러 객체에게 부여함으로써 객체 간의 결합도를 줄이는 데 목적이 있다. 이 패턴은 요청을 보내는 쪽과 이를 처리하는 쪽을 분리하고, 여러 객체를 연결하여 요청을 처리할 수 있는 체인을 형성한다. 어떤 객체가 요청을 처리할 수 없을 때는 다음 객체로 요청을 전달한다. 이 패턴의 이름은 실제로 책임을 연쇄적으로 전달한다는 점에서 유래했다.

     

    패턴 미적용 예시 코드

    게임 개발에서 책임 연쇄 패턴을 적용하지 않은 상황을 가정해보자. 예를 들어, 게임 내에서 캐릭터가 다양한 유형의 아이템을 사용하는 상황에서 각 아이템 유형별로 처리 로직을 갖는 경우다.

    public class ItemHandler
    {
        public void HandleItem(string itemType, string item)
        {
            if (itemType == "Potion")
            {
                Console.WriteLine($"Using {item} to heal.");
            }
            else if (itemType == "Shield")
            {
                Console.WriteLine($"Using {item} to defend.");
            }
            else if (itemType == "Sword")
            {
                Console.WriteLine($"Using {item} for attack.");
            }
            // 다른 아이템 유형들에 대한 처리가 이어짐
            else
            {
                Console.WriteLine("Unknown item type.");
            }
        }
    }
    

    이 코드에서 문제는 HandleItem 메서드가 너무 많은 책임을 지니고 있고, 새로운 아이템 유형이 추가될 때마다 메서드를 수정해야 한다는 점이다. 이는 개방-폐쇄 원칙(OCP)에 위배되며, 유지보수가 어렵다.

     

    패턴 적용 예시 코드

    책임 연쇄 패턴을 적용하면 각 아이템 처리 로직을 별도의 클래스로 분리하고, 이들을 연결하여 요청이 적절한 객체에 의해 처리되도록 할 수 있다.

    public abstract class ItemHandler
    {
        protected ItemHandler successor;
    
        public void SetSuccessor(ItemHandler successor)
        {
            this.successor = successor;
        }
    
        public abstract void HandleItem(string item);
    }
    
    public class PotionHandler : ItemHandler
    {
        public override void HandleItem(string item)
        {
            if (item == "Potion")
            {
                Console.WriteLine($"Using {item} to heal.");
            }
            else if (successor != null)
            {
                successor.HandleItem(item);
            }
        }
    }
    
    public class ShieldHandler : ItemHandler
    {
        public override void HandleItem(string item)
        {
            if (item == "Shield")
            {
                Console.WriteLine($"Using {item} to defend.");
            }
            else if (successor != null)
            {
                successor.HandleItem(item);
            }
        }
    }
    
    public class SwordHandler : ItemHandler
    {
        public override void HandleItem(string item)
        {
            if (item == "Sword")
            {
                Console.WriteLine($"Using {item} for attack.");
            }
            else if (successor != null)
            {
                successor.HandleItem(item);
            }
        }
    }
    
    // 클라이언트 코드
    public class Client
    {
        public static void Main(string[] args)
        {
            // 책임 연쇄 설정
            var potionHandler = new PotionHandler();
            var shieldHandler = new ShieldHandler();
            var swordHandler = new SwordHandler();
    
            potionHandler.SetSuccessor(shieldHandler);
            shieldHandler.SetSuccessor(swordHandler);
    
            // 요청 처리
            potionHandler.HandleItem("Potion");
            potionHandler.HandleItem("Shield");
            potionHandler.HandleItem("Sword");
        }
    }
    
    

     

     

    위 코드는 각 아이템 유형(Potion, Shield, Sword)을 처리하는 로직을 ItemHandler 추상 클래스를 상속받는 각각의 핸들러 클래스(PotionHandler, ShieldHandler, SwordHandler)로 분리했다. 이 구조에서, 아이템 처리 요청이 들어오면, 체인의 첫 번째 핸들러(PotionHandler)가 요청을 받아 처리할 수 있는지 확인한다. 처리할 수 없다면, 다음 핸들러(ShieldHandler, 그 다음은 SwordHandler)에게 요청을 넘긴다. 이렇게 각 핸들러가 요청을 순차적으로 처리할 수 있는지 확인하며, 적절한 핸들러가 요청을 처리한다.

     

    실행결과

    Using Potion to heal.
    Using Shield to defend.
    Using Sword for attack.

     

    이 접근 방식의 장점은 다음과 같다:

    • 확장성: 새로운 아이템 유형이 추가될 때, 기존 코드를 수정하지 않고 새로운 핸들러 클래스를 추가함으로써 요구사항을 쉽게 반영할 수 있다.
    • 낮은 결합도: 각 핸들러는 다음 핸들러에 대해 최소한만 알면 되므로, 코드 간 결합도가 낮아진다.
    • 유연성: 핸들러 체인의 구성을 실행 시간에 변경할 수 있어, 다양한 상황에 맞게 유연하게 대응할 수 있다.

    책임 연쇄 패턴을 사용함으로써, 요청을 처리하는 로직이 분산되어 코드의 가독성과 유지보수성이 향상된다. 또한, 각각의 처리 단계가 명확히 분리되어 있어, 복잡한 처리 과정을 보다 쉽게 관리할 수 있다.