Chain of Responsibility

chris·2021년 9월 11일
0

design pattern

목록 보기
2/2

Intent

Chain of Responsibility는 일련의 핸들러를 따라 요청을 전달할 수 있는 행동 디자인 패턴이다. 요청을 받으면 각 핸들러는 요청을 처리할지 아니면 체인의 다음 핸들러로 전달할지 결정한다.

Problem

온라인 주문 시스템에서 작업하고 있다고 상상해보자. 인증된 사용자만 주문을 생성할 수 있도록 시스템에 대ㄴ 액세스를 제한하려고 한다. 또한 관리 권한이 있는 사용자는 모든 주문에 대한 전체 액세스 권한이 있어야 한다.

약간의 계약 후에 이러한 검사를 순차적으로 수행해야 한다는 것을 깨달았다. 애플리케이션은 사용자의 자격 증명이 포함된 요청을 수신할 때마다 시스템에 대해 사용자 인증을 시도할 수 있다. 그러나 이러한 자격 증명이 올바르지 않고 인증에 실패하면 다른 검사를 진행할 이유가 없다.

다음 몇 달 동난 이러한 순차 검사 중 몇 가지를 더 구현했다.

  • 동료 중 한 명이 원시 데이터를 주문 시스템에 직접 전달하는 것은 안전하지 않다고 제안했다. 따라서 요청의 데이터를 삭제하기 위해 추가 유효성 검사 단계를 추가했다.

  • 나중에 누군가는 시스템이 무차별 암호 해독에 취약하다는 것을 알아차렸다. 이를 무효화하기 위해 동일한 IP 주소에서 오는 반복적인 실패한 요청을 필터링하는 검사를 즉시 추가했다.

  • 다른 누군가는 동일한 데이터가 포함된 반복 요청에 대해 캐시된 결과를 반환하여 시스템 속도를 높일 수 있다고 제안했다. 따라서 적절한 캐시 응답이 없는 경우에만 요청이 시스템으로 전달되도록 하는 또 다른 검사를 추가했다.

이미 엉망인 것처럼 보였던 검사 코드는 새로운 기능을 추가할 때마다 점점 더 부풀려졌다. 하나의 검사를 변경하면 때때로 다른 검사에 영향을 준다. 무엇보다도 시스템의 다른 구성 요소를 보호하기 위해 검사를 재사용하려고 할 때 해당 구성 요소에 일부 검사가 필요하지만 전부는 아니기 때문에 일부 코드를 복제해야 했다.

시스템은 이해하기 매우 어렵고 유지 관리 비용이 많이 든다. 당신은 어느 날 전체를 리팩토링하기로 결정할 때까지 한동안 코드와 씨름했다.

Solution

다른 많은 Behavioral design pattern과 마찬가지로 Chain of Responsibility는 특정 행동을 핸들러라고 하는 독립 실행형 개체로 변환하는데 의존한다. 우리의 경우 각 검사는 검사를 수행하는 단일 메서드를 사용하여 자체 클래스로 추출하여야 한다. 요청은 데이터와 함께 이 메서드에 인수로 전달된다.

패턴은 이러한 핸들러를 체인으로 연결하도록 제안한다. 연결된 각 핸들러에는 체인의 다음 핸들러에 대한 참조를 저장하기 위한 필드가 있다. 요청을 처리하는 것 외에도 핸들러는 체인을 따라 요청을 더 전달한다. 요청은 모든 핸들러가 처리할 기회를 가질 때까지 체인을 따라 이동한다.

가장 좋은 부분은 다음과 같다. 핸들러는 요청을 더이상 체인 아래로 전달하지 않고 추가 처리를 효과적으로 중지할 수 있다.

순서 지정 시스템이 있는 우리의 예에서 핸들러는 처리를 수행한 다음 요청을 체인 아래로 더 전달할지 여부를 결정한다. 요청에 올바른 데이터가 포함되어 있다고 가정하면 모든 핸들러는 인증 확인이든 캐싱이든 동작을 실행할 수 있다.

그러나 요청을 수신하면 처리하기가 처리할 수 있는지 여부를 결정하는 약간 다른 접근 방식이 있다(and it's a bit more canonical).가능한 경우 더 이상 요청을 전달하지 않는다. 따라서 요청을 처리하는 핸들러는 하나뿐이거나 전혀 처리하지 않는다. 이 접근 방식은 그래픽 사용자 인터페이스내에서 요소 스택의 이벤트를 처리할 때 매우 일반적이다.

예를 들어, 사용자가 버튼을 클릭하면 이벤트는 버튼으로 시작하여 해당 컨테이너(예: 양식 또는 패널)를 따라 이동하고 메인 애플리메이션 창으로 끝나는 GUI 요소 체인을 통해 전파된다. 이벤트는 처리할 수 있는 체인의 첫 번째 요소에 의해 처리된다. 이 예는 체인이 항상 개체 트리에서 추출될 수 있음을 보여주기 때문에 주목할 만하다.

모든 핸들러 클래스가 동일한 인터페이스를 구현하는 것이 중요하다. 각 구체적인 핸들러는 execute 메서드가 있는 다음 핸들러에만 신경을 써야 한다. 이렇게 하면 코드를 구체적인 클래스에 연결하지 않고도 다양한 핸들러를 사용하여 런타임에 체인을 구성할 수 있다.

Real-World Analogy


컴퓨터에 새 하드웨어를 구입하여 설치했다. 당신은 괴짜이기 때문에 컴퓨터에는 여러 운영 체제가 설치되어 있다. 하드웨어가 지원되는지 확인하기 위해 모두 부팅을 시도한다. Windows는 하드웨어를 자동으로 감지하고 활성화 한다. 그러나 사랑하는 Linux는 하드웨어로 작업하는 것을 거부한다. 작은 희망을 품고 상자에 적힌 기술 지원 전화번호로 전화하기로 결정한다.

가장 먼저 들리는 것은 자동 응답기의 로봇 음성이다. 다양한 문제에 대한 9가지 인기 있는 솔루션을 제안하지만 그 중 어느 것도 귀하의 사례와 관련이 없다. 잠시후 로봇이 당신을 라이브 교환원에게 연결한다.
Alas, 교환원도 구체적인 제안을 할 수 없다. 그는 매뉴얼에서 긴 발췌문을 계속 인용하며 당신의 의견을 듣지 않는다. "컴퓨터를 껐다가 다시 켜보셨습니까?"라는 문구를 듣고 10번째로, 단신은 적절한 엔지니어에게 연결을 요구한다.

결국 교환원은 사무실 건물의 어두운 지하실에 있는 외로운 서비실에 앉아 몇 시간 동안 실시간 채팅을 갈망했던 엔지니어 중 한 명에게 전화를 전달한다. 엔지니어는 새 하드웨어에 적합한 드라이버를 다운로드할 위치와 Linux에 설치하는 방법을 알려준다. 마지막으로 솔루션!기쁨에 넘쳐 통화를 종료한다.

Structure


1. Handler는 모든 구체적인 핸들러에 공통적인 인터페이스를 선언한다. 일반적으로 요청을 처리하는 단일 메서드만 포함하지만 때로는 체인에서 다음 핸들러를 설정하기 위한 다른 메서드가 있을 수도 있다.

2. Base Handler는 모든 핸들러 클래스에 공통적인 상용구 코드를 넣을 수 있는 선택적 클래스다.

일반적으로 이 클래스는 다음 핸들러에 대한 참조를 저장하기 위한 필드를 정의한다. 클라이언트는 핸들러를 이전 핸들러의 생성자 또는 설정자에 전달하여 체인을 구축할 수 있다. 클래스는 기본 처리 동작을 구현할 수도 있다. 즉, 존재 여부를 확인한 후 다음 처리기로 실행을 전달할 수 있다.

3. Concrete Handler에는 요청을 처리하기 위한 실제 코드가 포함되어 있다. 요청을 수신하면 각 핸들러는 요청을 처리할지 여부와 함께 체인을 따라 전달할지 여부를 결정해야 한다.

핸들러는 일반적으로 자체 포함되고 변경할 수 없으며 생성자를 통해 필요한 모든 데이터를 한 번만 수락한다.

4. Client는 응용 프로그램의 논리에 따라 체인을 한 번만 구성하거나 동적으로 구성할 수 있다. 요청은 체인의 모든 핸들러로 보낼 수 있다. 첫 번째 핸들러일 필요는 없다.

Pseudocode

이 예에서 Chain of Responsibility 패턴은 활성 GUI 요소에 대한 상황별 도움말 정보를 표시하는 역할을 한다.

응용 프로그램의 GUI는 일반적으로 개체 트리로 구성된다. 예를 들어 앱의 기본 창을 렌더링하는 Dialog 클래스는 개체 트리의 루트가 된다. 대화 상자에는 다른 Panel이나 ButtonTextField와 같은 간단한 하위 수준 요소가 포함될 수 있는 패널이 포함되어 있다.
간단한 구성 요소는 구성 요소에 일부 도움말 텍스트가 할당되어 있는 한 간단한 상황별 도구 설명을 표시할 수 있다. 그러나 더 복잡한 구성 요소는 설명서에서 발췌한 내용을 표시하거나 브라우저에서 페이지를 여는 것과 같이 상황에 맞는 도움말을 표시하는 고유한 방법을 정의한다.

사용자가 요소에 마우스 커서를 놓고 F1 키를 누르면 응용 프로그램이 포인터 아래의 구성 요소를 감지하고 도움말 요청을 보낸다. 요청은 도움말 정보를 표시할 수 있는 요소에 도달할 때까지 모든 요소의 컨테이너를 통해 버블링 된다.

// The handler interface declares a method for building a chain
// of handlers. It also declares a method for executing a
// request.
interface ComponentWithContextualHelp is
    method showHelp()


// The base class for simple components.
abstract class Component implements ComponentWithContextualHelp is
    field tooltipText: string

    // The component's container acts as the next link in the
    // chain of handlers.
    protected field container: Container

    // The component shows a tooltip if there's help text
    // assigned to it. Otherwise it forwards the call to the
    // container, if it exists.
    method showHelp() is
        if (tooltipText != null)
            // Show tooltip.
        else
            container.showHelp()


// Containers can contain both simple components and other
// containers as children. The chain relationships are
// established here. The class inherits showHelp behavior from
// its parent.
abstract class Container extends Component is
    protected field children: array of Component

    method add(child) is
        children.add(child)
        child.container = this


// Primitive components may be fine with default help
// implementation...
class Button extends Component is
    // ...

// But complex components may override the default
// implementation. If the help text can't be provided in a new
// way, the component can always call the base implementation
// (see Component class).
class Panel extends Container is
    field modalHelpText: string

    method showHelp() is
        if (modalHelpText != null)
            // Show a modal window with the help text.
        else
            super.showHelp()

// ...same as above...
class Dialog extends Container is
    field wikiPageURL: string

    method showHelp() is
        if (wikiPageURL != null)
            // Open the wiki help page.
        else
            super.showHelp()


// Client code.
class Application is
    // Every application configures the chain differently.
    method createUI() is
        dialog = new Dialog("Budget Reports")
        dialog.wikiPageURL = "http://..."
        panel = new Panel(0, 0, 400, 800)
        panel.modalHelpText = "This panel does..."
        ok = new Button(250, 760, 50, 20, "OK")
        ok.tooltipText = "This is an OK button that..."
        cancel = new Button(320, 760, 50, 20, "Cancel")
        // ...
        panel.add(ok)
        panel.add(cancel)
        dialog.add(panel)

    // Imagine what happens here.
    method onF1KeyPress() is
        component = this.getComponentAtMouseCoords()
        component.showHelp()

Applicability

프로그램이 다양한 방식으로 다양한 종류의 요청을 처리할 것으로 예상되지만 정확한 요청 유형과 순서를 미리 알 수 없는 경우 책임 사슬 패턴을 사용한다.

이 패턴을 사용하면 여러 핸들러를 하나의 체인으로 연결할 수 있으며 요청을 받으면 각 핸들러가 처리할 수 있는지 "요청"한다. 이런 식으로 모든 핸들러는 요청을 처리할 기회를 얻는다.

특정 순서로 여러 핸들러를 실행해야 하는 경우 패턴을 사용합니다.

어떤 순서로든 체인의 핸들러를 연결할 수 있으므로 모든 요청은 계획한 대로 정확히 체인을 통과한다.

핸들러 세트와 그 순서가 런타임에 변경되어야 하는 경우 CoR 패턴을 사용하십시오.

핸들러 클래스 내부의 참조 필드에 대한 세터를 제공하면 핸들러를 동적으로 삽입, 제거 또는 재정렬할 수 있다.

How to Implement

  1. 핸들러 인터페이스를 선언하고 요청을 처리하는 메서드의 서명을 설명한다.


    클라이언트가 요청 데이터를 메소드에 전달하는 방법을 결정해야 한다. 가장 유연한 방법은 요청을 객체로 변환하여 처리 메소드에 인수로 전달하는 것이다.

  2. 구체적인 핸들러에서 중복된 상용구 코드를 제거하려면 핸들러 인터페이스에서 파생된 추상 기본 핸들러 클래스를 만드는 것이 좋다.


    이 클래스에는 체인의 다음 핸들러에 대한 참조를 저장하기 위한 필드가 있어야 한다. 클래스를 불변으로 만드는 것을 고려해야 한다. 그러나 런타임에 체인을 수정하려는 경우 참조 필드의 값을 변경하기 위한 setter를 정의해야 한다.


    아무 것도 남지 않는 한 다음 객체로 요청을 전달하는 처리 방법에 대한 편리한 기본 동작을 구현할 수도 있다. 구체 처리기는 부모 메서드를 호출하여 이 동작을 사용할 수 있다.

  3. 하나씩 구체적인 처리기 하위 클래스를 만들고 처리 방법을 구현한다. 각 처리기는 요청을 수신할 때 두 가지 결정을 내려야 한다.

    • 요청을 처리할지 여부.
    • 체인을 따라 요청을 전달할지 여부.

  4. 클라이언트는 자체적으로 체인을 어셈블하거나 다른 개체에서 미리 빌드된 체인을 수신할 수 있다. 후자의 경우 구성 또는 환경 설정에 따라 체인을 빌드하기 위해 일부 팩토리 클래스를 구현해야 한다.

  5. 클라이언트는 첫 번째 핸들러뿐만 아니라 체인의 모든 핸들러를 트리거할 수 있다. 요청은 일부 핸들러가 더 이상 전달을 거부하거나 체인 끝에 도달할 때까지 체인을 따라 전달된다.

  6. 체인의 동적 특성으로 인해 클라이언트는 다음 시나리오를 처리할 준비가 되어 있어야 한다.

    • 체인은 단일 링크로 구성될 수 있다.
    • 일부 요청은 체인 끝에 도달하지 못할 수 있다.
    • 다른 것들은 처리되지 않은 채 체인의 끝에 도달할 수 있다.

Pros and Cons

O. 요청 처리 순서를 제어할 수 있다.
O. 단일 책임 원칙. 작업을 수행하는 클래스에서 작업을 호출하는 클래스를 분리할 수 있다.
X. 개방/폐쇄 원칙. 기존 클라이언트 코드를 손상시키지 않고 앱에 새 핸들러를 도입할 수 있다.

Relations with Other Patterns

  • Chain of Responsibility, Command, MediatorObserver는 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룬다.

    • Chain of Responsibility는 잠재적 수신자 중 하나가 처리할 때까지 잠재적 수신자의 동적 사슬을 따라 순차적으로 요청을 전달한다.
    • Command는 발신자와 수신자 간의 단방향 연결을 설정한다.
    • Mediator는 송신자와 수신자 간의 직접 연결을 제거하여 중재자 개체를 통해 간접적으로 통신하도록 한다.
    • Observer를 사용하면 수신기가 수신 요청을 동적으로 구독 및 구독 취소할 수 있다.

  • Chain of Responsibility는 종종 Composite와 함께 사용된다. 이 경우 리프 구성 요소가 요청을 받으면 모든 상위 구성 요소의 체인을 통해 개체 트리의 루트까지 전달할 수 있다.

  • Chain of Responsibility의 핸들러는 Command로 구현할 수 있다. 이 경우 요청으로 표시되는 동일한 컨텍스트 개체에 대해 다양한 작업을 실행할 수 있다.


    그러나 요청 자체가 Command 개체인 또 다른 접근 방식이 있다. 이 경우 체인에 연결된 일련의 서로 다른 컨텍스트에서 동일한 작업을 실행할 수 있다.

  • Chain of ResponsibilityDecorator는 클래스 구조가 매우 유사하다. 두 패턴 모두 일련의 개체를 통해 실행을 전달하기 위해 재귀 구성에 의존한다. 그러나 몇 가지 결정적인 차이점이 있다.


    CoR 핸들러는 서로 독립적으로 임의의 작업을 실행할 수 있다. 또한 언제든지 요청을 더 이상 전달하지 않을 수 있다. 반면에 다양한 데코레이터는 기본 인터페이스와 일관성을 유지하면서 객체의 동작을 확장할 수 있다. 또한 데코레이터는 요청의 흐름을 중단할 수 없다.
profile
software engineer

0개의 댓글