Chain of Responsibility

Chain of Responsibility Pattern이란?

책임 연쇄 패턴Chain of Responsibility Pattern은 요청을 처리할 수 있는 객체들이 연쇄적으로 연결된 구조에서, 요청이 처리될 수 있는 객체를 찾을 때까지 요청을 전달하는 방식의 패턴입니다. 이 패턴은 요청을 처리하는 객체가 하나 이상일 수 있으며, 각 객체는 요청을 처리하거나, 다음 객체에게 요청을 전달할 수 있습니다. 이 패턴의 주요 목적은 요청을 보내는 클라이언트와 요청을 처리하는 객체 사이의 결합을 느슨하게 만들고, 유연한 구조로 요청을 처리할 수 있도록 돕는 것입니다.

Chain of Responsibility의 필요성

책임 연쇄 패턴은 다음과 같은 상황에서 필요합니다:

요청을 처리하는 방법이 여러 단계로 나뉘어 있을 때

요청을 단계별로 처리하는 여러 객체가 있을 때, 요청이 각 단계에서 처리되는지 확인하는 구조를 만들 때 유용합니다.

요청을 처리할 객체가 동적으로 변경될 수 있을 때

클라이언트 코드가 요청을 처리할 구체적인 객체를 알 필요가 없고, 요청이 적절한 객체에서 처리되도록 할 수 있습니다.

Chain of Responsibility 구조

D2 Diagram

  • Client → Handler
    • 클라이언트가 요청을 처음 Handler에 전달합니다.
  • Handler → ConcreteHandlerA
    • 요청이 첫 번째 핸들러에게 전달됩니다.
  • ConcreteHandlerA → Handler (If Handled)
    • 요청이 처리되었으면 핸들러에게 반환되거나 체인이 종료됩니다. 이 동작은 옵션으로 표시됩니다.
  • ConcreteHandlerA → ConcreteHandlerB
    • 첫 번째 핸들러가 요청을 처리하지 못하면 다음 핸들러로 요청이 전달됩니다.
  • ConcreteHandlerB
    • 두 번째 핸들러에서 동일한 로직으로 요청을 처리하거나 다음 핸들러로 전달됩니다.
  • ConcreteHandlerC → Handler
    • 마지막 핸들러가 요청을 처리했는지 여부에 따라 핸들러에게 반환되거나 체인이 종료됩니다.

Chain of Responsibility의 구성 요소

책임 연쇄 패턴은 다음과 같은 구성 요소로 이루어집니다:

핸들러Handler

요청을 처리하거나 다음 핸들러로 요청을 전달하는 인터페이스를 정의합니다.

구체적인 핸들러Concrete Handler

핸들러 인터페이스를 구현하며, 요청을 처리할 수 있는 구체적인 로직을 포함합니다. 요청을 처리하지 못하면, 다음 핸들러에게 요청을 전달합니다.

클라이언트Client

책임 연쇄를 설정하고, 요청을 첫 번째 핸들러에게 전달합니다.

Chain of Responsibility 적용

잘못된 요청 처리 방식

책임 연쇄 패턴을 적용하지 않고, 모든 요청을 처리하는 객체를 명시적으로 지정하는 경우, 코드가 복잡해지고 변경에 취약해질 수 있습니다. 예를 들어, 도서 관리 시스템에서 책의 요청 처리를 각 단계마다 처리하는 예시입니다.

// 모든 요청을 하나의 클래스에서 처리하는 방식
public class BookRequestHandler
{
    public void HandleRequest(string requestType, string book)
    {
        if (requestType == "borrow")
        {
            Console.WriteLine($"Book '{book}' borrowed.");
        }
        else if (requestType == "return")
        {
            Console.WriteLine($"Book '{book}' returned.");
        }
        else if (requestType == "reserve")
        {
            Console.WriteLine($"Book '{book}' reserved.");
        }
        else
        {
            Console.WriteLine($"Request '{requestType}' for book '{book}' is invalid.");
        }
    }
}

코드 설명 및 문제점

확장성 부족

새로운 요청 유형을 처리하려면 기존 코드를 수정해야 하므로, 코드의 확장성이 떨어집니다.

단일 책임 원칙 위반

BookRequestHandler 클래스가 너무 많은 책임을 가지고 있어 유지보수가 어렵습니다.

Chain of Responsibility 적용 예시

책임 연쇄 패턴을 사용하면, 각 요청을 처리하는 핸들러를 개별적으로 정의하고, 핸들러들을 체인 형태로 연결할 수 있습니다.

// 핸들러 인터페이스
public abstract class RequestHandler
{
    protected RequestHandler _nextHandler;
    public void SetNext(RequestHandler nextHandler)
    {
        _nextHandler = nextHandler;
    }
    public abstract void HandleRequest(string requestType, string book);
}
// 구체적인 핸들러: 대출 처리
public class BorrowHandler : RequestHandler
{
    public override void HandleRequest(string requestType, string book)
    {
        if (requestType == "borrow")
        {
            Console.WriteLine($"Book '{book}' borrowed.");
        }
        else if (_nextHandler != null)
        {
            _nextHandler.HandleRequest(requestType, book);
        }
    }
}
// 구체적인 핸들러: 반납 처리
public class ReturnHandler : RequestHandler
{
    public override void HandleRequest(string requestType, string book)
    {
        if (requestType == "return")
        {
            Console.WriteLine($"Book '{book}' returned.");
        }
        else if (_nextHandler != null)
        {
            _nextHandler.HandleRequest(requestType, book);
        }
    }
}
// 구체적인 핸들러: 예약 처리
public class ReserveHandler : RequestHandler
{
    public override void HandleRequest(string requestType, string book)
    {
        if (requestType == "reserve")
        {
            Console.WriteLine($"Book '{book}' reserved.");
        }
        else if (_nextHandler != null)
        {
            _nextHandler.HandleRequest(requestType, book);
        }
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        // 핸들러 체인 설정
        RequestHandler borrowHandler = new BorrowHandler();
        RequestHandler returnHandler = new ReturnHandler();
        RequestHandler reserveHandler = new ReserveHandler();
        borrowHandler.SetNext(returnHandler);
        returnHandler.SetNext(reserveHandler);
        // 요청 처리
        borrowHandler.HandleRequest("borrow", "Design Patterns");
        borrowHandler.HandleRequest("return", "Refactoring");
        borrowHandler.HandleRequest("reserve", "Clean Code");
    }
}

RequestHandler 클래스

핸들러의 추상 클래스입니다. 이 클래스는 다음 핸들러로 요청을 전달할 수 있는 메커니즘을 제공합니다.

BorrowHandler, ReturnHandler, ReserveHandler 클래스

각 핸들러는 특정 요청을 처리하고, 처리하지 못하면 다음 핸들러로 요청을 전달합니다.

클라이언트 코드

핸들러 체인을 설정하고, 요청을 첫 번째 핸들러에게 전달합니다.

Chain of Responsibility 장단점

장점

유연한 요청 처리

요청을 처리할 핸들러가 동적으로 변경될 수 있으며, 새로운 요청 유형을 추가할 때도 기존 코드를 수정하지 않고 확장할 수 있습니다.

단일 책임 원칙 준수

각 핸들러가 하나의 책임만 가지므로, 코드의 유지보수성이 높아집니다.

클라이언트 코드의 단순화

클라이언트는 핸들러의 구체적인 처리를 알 필요 없이, 요청을 처리할 수 있습니다.

단점

요청이 처리되지 않을 수 있음

체인에 요청을 처리할 핸들러가 없으면, 요청이 처리되지 않고 끝날 수 있습니다.

디버깅 어려움

핸들러 체인 내에서 요청이 어떻게 처리되는지 추적하기가 어렵습니다.

단점 해결 방안

기본 핸들러 설정

요청이 체인에서 처리되지 않았을 경우, 기본 핸들러를 설정하여 처리가 누락되지 않도록 할 수 있습니다.

// 기본 핸들러: 요청 처리 불가 시 메시지 출력
public class DefaultHandler : RequestHandler
{
    public override void HandleRequest(string requestType, string book)
    {
        Console.WriteLine($"Request '{requestType}' for book '{book}' could not be processed.");
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        RequestHandler borrowHandler = new BorrowHandler();
        RequestHandler returnHandler = new ReturnHandler();
        RequestHandler reserveHandler = new ReserveHandler();
        RequestHandler defaultHandler = new DefaultHandler();
        borrowHandler.SetNext(returnHandler);
        returnHandler.SetNext(reserveHandler);
        reserveHandler.SetNext(defaultHandler);
        borrowHandler.HandleRequest("sell", "Some Book");  // 처리되지 않으면 기본 핸들러가 호출됨
    }
}

디버깅을 위한 로깅

각 핸들러가 요청을 처리할 때, 로깅 기능을 추가하여 체인 내에서 요청이 어떻게 처리되는지 추적할 수 있습니다.

// 로깅 기능 추가된 핸들러
public class LoggingHandler : RequestHandler
{
    public override void HandleRequest(string requestType, string book)
    {
        Console.WriteLine($"Request received: {requestType} for book {book}");
        if (_nextHandler != null)
        {
            _nextHandler.HandleRequest(requestType, book);
        }
    }
}
// 클라이언트 코드
public class Program
{
    public static void Main(string[] args)
    {
        RequestHandler loggingHandler = new LoggingHandler();
        RequestHandler borrowHandler = new BorrowHandler();
        RequestHandler returnHandler = new ReturnHandler();
        RequestHandler reserveHandler = new ReserveHandler();
        loggingHandler.SetNext(borrowHandler);
        borrowHandler.SetNext(returnHandler);
        returnHandler.SetNext(reserveHandler);
        loggingHandler.HandleRequest("borrow", "Design Patterns");
    }
}

객체지향 원칙과의 관계

단일 책임 원칙

각 핸들러는 하나의 요청 처리 로직만 담당하여 단일 책임 원칙을 잘 준수합니다.

개방_폐쇄 원칙

새로운 핸들러를 추가해도 기존 코드를 수정하지 않고 확장할 수 있으므로 개방-폐쇄 원칙을 따릅니다.

의존 역전 원칙

클라이언트는 구체적인 핸들러 클래스에 의존하지 않고, 핸들러의 추상 인터페이스에 의존하므로 의존 역전 원칙을 충족합니다.

맺음말

책임 연쇄 패턴은 요청을 처리할 객체가 여러 개일 때, 유연하고 확장 가능한 구조를 제공하는 강력한 패턴입니다. 요청 처리의 흐름을 유연하게 만들고, 각 핸들러를 독립적으로 처리할 수 있어 시스템의 유지보수성과 확장성을 높입니다.

심화학습

요청 필터링

책임 연쇄 패턴은 요청 필터링에도 유용합니다. 예를 들어, 도서 관리 시스템에서 사용자의 역할(관리자, 일반 사용자 등)에 따라 특정 요청을 필터링할 수 있습니다. 이를 통해 요청이 적절한 사용자에 의해서만 처리되도록 할 수 있습니다.

// 구체적인 핸들러: 관리자 역할 필터링
public class AdminRoleHandler : RequestHandler
{
    public override void HandleRequest(string requestType, string book)
    {
        if (requestType == "delete" && !IsAdmin())
        {
            Console.WriteLine("Unauthorized request. Only admins can delete books.");
        }
        else if (_nextHandler != null)
        {
            _nextHandler.HandleRequest(requestType, book);
        }
    }
    private bool IsAdmin()
    {
        // 실제 관리자 확인 로직 (예: 사용자 세션 확인)
        return false;
    }
}

위 예시에서 AdminRoleHandler는 사용자가 관리자인지 확인하고, 관리자가 아닌 경우 요청을 처리하지 않습니다. 이처럼 패턴을 사용해 요청을 필터링하고 권한을 관리하는 로직을 추가할 수 있습니다.

비동기 처리

책임 연쇄 패턴은 비동기 요청 처리에서도 효과적으로 사용될 수 있습니다. 각 핸들러가 비동기적으로 요청을 처리하거나 다음 핸들러로 전달하는 방식으로 확장할 수 있습니다.

public abstract class AsyncRequestHandler
{
    protected AsyncRequestHandler _nextHandler;
    public void SetNext(AsyncRequestHandler nextHandler)
    {
        _nextHandler = nextHandler;
    }
    public abstract Task HandleRequestAsync(string requestType, string book);
}
public class AsyncBorrowHandler : AsyncRequestHandler
{
    public override async Task HandleRequestAsync(string requestType, string book)
    {
        if (requestType == "borrow")
        {
            await Task.Run(() => Console.WriteLine($"Book '{book}' borrowed asynchronously."));
        }
        else if (_nextHandler != null)
        {
            await _nextHandler.HandleRequestAsync(requestType, book);
        }
    }
}

이 코드는 요청을 비동기적으로 처리하는 핸들러 체인을 구현한 예시입니다. AsyncRequestHandler 클래스는 비동기 Task를 사용해 요청을 처리하고, 다음 핸들러로 요청을 전달합니다.

핸들러 상태 관리

책임 연쇄 패턴에서 각 핸들러는 상태를 유지하며 요청을 처리할 수 있습니다. 예를 들어, 도서 관리 시스템에서 특정 책의 상태(대출 가능, 반납됨 등)를 기반으로 요청을 처리할 수 있습니다. 각 핸들러가 요청 처리 시 내부 상태를 관리하면서 다음 핸들러에게 필요한 정보를 전달할 수 있습니다.

// 상태를 기반으로 요청을 처리하는 핸들러
public class BookStatusHandler : RequestHandler
{
    private bool _isBorrowed;
    public override void HandleRequest(string requestType, string book)
    {
        if (requestType == "borrow" && _isBorrowed)
        {
            Console.WriteLine($"Book '{book}' is already borrowed.");
        }
        else if (requestType == "borrow")
        {
            _isBorrowed = true;
            Console.WriteLine($"Book '{book}' borrowed successfully.");
        }
        else if (_nextHandler != null)
        {
            _nextHandler.HandleRequest(requestType, book);
        }
    }
}

위 코드에서 BookStatusHandler는 책의 대출 여부를 관리하며 요청을 처리합니다. 상태 기반 요청 처리는 핸들러가 요청에 대한 컨텍스트를 이해하고 처리하는 데 도움이 됩니다.

핸들러 체인의 동적 구성

핸들러 체인은 고정된 순서로 설정될 필요가 없습니다. 상황에 따라 동적으로 핸들러 체인을 구성할 수 있으며, 이를 통해 다양한 요청 처리 흐름을 유연하게 설정할 수 있습니다.

// 클라이언트 코드에서 핸들러 체인 동적으로 구성
public class Program
{
    public static void Main(string[] args)
    {
        RequestHandler borrowHandler = new BorrowHandler();
        RequestHandler returnHandler = new ReturnHandler();
        RequestHandler reserveHandler = new ReserveHandler();
        // 동적으로 핸들러 체인 구성
        bool handleReturns = false;
        borrowHandler.SetNext(handleReturns ? returnHandler : reserveHandler);
        // 요청 처리
        borrowHandler.HandleRequest("borrow", "Clean Code");
    }
}

연쇄 구조의 순회 전략

책임 연쇄 패턴에서는 요청을 여러 단계로 나누어 처리할 수 있습니다. 하지만 모든 요청이 반드시 하나의 핸들러에서 끝나는 것은 아니며, 여러 핸들러가 순차적으로 같은 요청을 처리할 수 있는 구조도 가능합니다. 이런 경우 각 핸들러가 처리 후에도 다음 핸들러로 요청을 계속 전달하도록 설계할 수 있습니다.

public class AllHandlersProcessRequest : RequestHandler
{
    public override void HandleRequest(string requestType, string book)
    {
        Console.WriteLine($"Processing '{requestType}' for book '{book}' in {this.GetType().Name}.");
        
        // 다음 핸들러로 요청 전달
        if (_nextHandler != null)
        {
            _nextHandler.HandleRequest(requestType, book);
        }
    }
}