프로그래밍 이론/디자인 패턴

13. [Design Pattern] 책임 연쇄(Chain of Responsibility) 패턴

NewtronVania 2024. 1. 6. 03:17

책임 연쇄 패턴

요청을 처리할 있는 여러 객체들 사이에서 요청을 전달하는 디자인 패턴. 패턴을 사용하면 요청을 보내는 쪽과 이를 처리하는 쪽을 분리할 있으며, 요청을 처리할 객체가 누구인지 클라이언트는 몰라도 된다.


구조

  • Handler: 모든 처리기(Handler)에 공통적인 인터페이스를 정의한다. 이 인터페이스는 일반적으로 요청을 처리하는 메소드를 포함한다.
  • ConcreteHandler: 실제 요청을 처리하는 클래스. 만약 해당 클래스가 요청을 처리할 수 없다면 다음 처리기로 요청을 전달한다.
  • Client: 요청을 처리기 체인에 보내는 역할. 클라이언트는 체인의 어떤 부분이 요청을 처리하는지 필요가 없다.

기본 인터페이스

// Handler 인터페이스
public interface IHandler
{
    void SetNext(IHandler handler);
    void HandleRequest(string request);
}

// ConcreteHandler 클래스
public class ConcreteHandler1 : IHandler
{
    private IHandler _nextHandler;

    public void SetNext(IHandler handler)
    {
        _nextHandler = handler;
    }

    public void HandleRequest(string request)
    {
        if (CanHandle(request))
        {
            Console.WriteLine($"Handled by ConcreteHandler1");
        }
        else
        {
            _nextHandler?.HandleRequest(request);
        }
    }

    private bool CanHandle(string request)
    {
        // 요청을 처리할 수 있는지 여부를 결정하는 로직
    }
}

// 클라이언트 클래스
public class Client
{
    public void Main()
    {
        IHandler handler1 = new ConcreteHandler1();
        IHandler handler2 = new ConcreteHandler2();

        handler1.SetNext(handler2);

        handler1.HandleRequest("request");
    }
}

왜 사용해야 할까?

책임 연쇄 패턴은 여러 객체에 걸쳐 요청 처리의 책임을 분산시키는 디자인 패턴으로, 이를 통해 각 객체는 요청을 처리하거나 다음 객체로 전달할 수 있는 유연성을 갖는다. 이 패턴의 주요 사용 이유는 각 객체가 자신의 책임을 갖고, 처리할 수 없는 요청을 다음 객체에게 넘길 수 있기 때문에, 복잡한 조건부 로직이나 if-else 체인을 피할 수 있다는 것이다. 이러한 접근 방식은 코드의 재사용성을 높이고, 유지보수를 용이하게 하며, 시스템의 확장성을 향상시킨다.

이를 통해 요청 처리 객체들 간의 결합도가 낮아지고, 객체는 단일 책임 원칙에 따라 설계될 있다. 또한, 요청 처리 체인에 새로운 객체를 추가하는 것이 간단해지므로, 시스템의 확장성이 상승한다. 

한 예시로, 사용자 지원 시스템에서 다양한 유형의 문의를 처리하는 경우를 생각해볼 수 있다. 각 문의는 먼저 기초적인 문제 해결 담당자에게 전달되고, 그들이 처리할 수 없는 문제는 더 고급 기술 지원 팀으로 이동한다.

// 처리자 인터페이스
public interface ISupportHandler
{
    ISupportHandler SetNext(ISupportHandler handler);
    string HandleSupport(string issueType);
}

// 기초 문제 해결 처리자
public class BasicSupportHandler : ISupportHandler
{
    private ISupportHandler _nextHandler;

    public ISupportHandler SetNext(ISupportHandler handler)
    {
        _nextHandler = handler;
        return handler;
    }

    public string HandleSupport(string issueType)
    {
        if (issueType == "Basic")
        {
            return "BasicSupportHandler: 문제 해결";
        }
        else
        {
            return _nextHandler?.HandleSupport(issueType);
        }
    }
}

// 고급 기술 지원 처리자
public class AdvancedSupportHandler : ISupportHandler
{
    private ISupportHandler _nextHandler;

    public ISupportHandler SetNext(ISupportHandler handler)
    {
        _nextHandler = handler;
        return handler;
    }

    public string HandleSupport(string issueType)
    {
        if (issueType == "Advanced")
        {
            return "AdvancedSupportHandler: 고급 문제 해결";
        }
        else
        {
            return _nextHandler?.HandleSupport(issueType);
        }
    }
}

BasicSupportHandler AdvancedSupportHandler 서로 다른 유형의 지원 문제를 처리한다. 문제 유형에 따라 적절한 처리자가 요청을 처리하며, 처리할 없는 문제는 다음 처리자에게 전달된다. 클라이언트는 처리자 체인의 번째 요소에 요청을 보내고, 요청은 체인을 통해 적절한 처리자에 도달하게 된다.

// 클라이언트 코드
class Client
{
    public static void Main(string[] args)
    {
        var basicSupport = new BasicSupportHandler();
        var advancedSupport = new AdvancedSupportHandler();

        basicSupport.SetNext(advancedSupport);

        // 기초적인 문제 요청
        Console.WriteLine(basicSupport.HandleSupport("Basic"));

        // 고급 기술 지원 필요한 문제 요청
        Console.WriteLine(basicSupport.HandleSupport("Advanced"));
    }
}

 

그러나 체인이 길어지면 성능 문제가 발생할 있으며, 요청 처리 과정이 복잡해질 있다. 따라서 적절한 상황에서 적절하게 사용되어야 한다.

장점

  • 클라이언트는 요청을 처리하는 구체적인 클래스를 몰라도 된다. 이는 클라이언트와 서버 사이의 결합도를 감소시킨다.
  • 처리기를 재사용하거나 조합하여 다양한 방식으로 요청을 처리할 있다.

단점

  • 요청이 체인의 많은 핸들러를 거치면서 지연될 수 있다.
  • 어떤 핸들러가 요청을 처리했는지 추적하기 어려울 있으며, 이는 디버깅을 어렵게 만든다.

전체 예시 코드

참고자료

 

GoF의 디자인 패턴 - 예스24

이 책은 디자인 패턴을 다룬 이론서로 디자인 패턴의 기초적이고 전반적인 내용을 학습할 수 있다.

www.yes24.com