[Design Pattern] 책임 연쇄 패턴

olwooz·2023년 2월 22일
0
핸들러의 체인을 따라 요청을 보낼 수 있게 해주는 행동 패턴
각 핸들러는 요청을 받으면 요청을 처리할 지 체인의 다음 핸들러에게 넘길지 결정

문제

온라인 주문 시스템 가정

  • 시스템에 대한 접근을 제한해 인증된 유저들만 주문할 수 있게 하고 싶은 상황
  • 또한 관리자 권한을 가진 유저들에게는 모든 주문에 대한 접근 권한이 주어져야 하는 상황

검사가 순차적으로 진행되어야 함 → 애플리케이션은 유저의 자격 증명을 포함한 요청을 받으면 인증 시도를 할 수 있지만, 자격 증명이 올바르지 않아 인증이 실패하면 다른 검사를 할 필요가 없어짐

순차적 검사들을 몇 가지 더 구현:

  • 주문 시스템에 raw 데이터를 그대로 전달하는 게 안전하지 않기 때문에 요청 데이터를 정제하는 검증 단계 추가
  • 브루트 포스 공격을 막기 위해 같은 IP로부터의 반복적인 요청 실패를 필터링하는 검사 추가
  • 캐시가 없을 때만 요청을 시스템으로 전달하는 검사 추가

검사 코드들은 새로운 기능이 추가될 때마다 더 비대해짐

한 검사를 변경하면 다른 검사에도 영향을 미침

다른 컴포넌트에서 검사를 재사용하려 할 때, 일부분의 검사만 필요하기 때문에 코드의 일부분만을 복사해서 사용해야만 함

→ 시스템이 복잡해지고 유지보수가 어려워져서 리팩토링을 하기로 결심

해결책

책임 연쇄 패턴은 특정 행동들을 핸들러라는 독립 객체로 변경

위 예제에서 각 검사는 검사를 수행하는 단일 메서드를 가지는 독립적인 클래스로 추출될 수 있음

  • 요청과 데이터는 메서드의 매개변수로 전달됨

책임 연쇄 패턴은 핸들러들을 체인으로 연결해 각 핸들러가 다음 핸들러에 대한 참조를 저장하는 필드를 가짐

  • 핸들러들은 요청을 처리하고, 체인을 따라서 요청을 더 멀리 전달함
  • 요청은 모든 핸들러들이 해당 요청을 처리할 기회를 가질 때까지 체인을 따라 이동
  • 핸들러는 요청 전달을 멈춰 효율적으로 처리를 중단할 수 있음

더 정석적인 방법 - 핸들러가 요청을 받으면 요청을 처리할 수 있는지 결정하고, 할 수 있다면 요청을 더 멀리 보내지 않음 → 단 하나의 핸들러만 요청을 처리하거나, 아무 핸들러도 처리하지 않음

  • GUI 요소들의 스택에서 이벤트를 처리하는 흔한 방법
  • e.g. 유저가 버튼을 클릭하면 이벤트가 버튼으로 시작헤 컨테이너를 거쳐 메인 애플리케이션 윈도우까지 가는 GUI 요소들의 체인을 따라 전파됨 → 체인에서 이벤트를 처리할 수 있는 첫 번째 요소가 처리하게 됨
  • 체인은 객체 트리에서 항상 추출될 수 있음

모든 핸들러 클래스들이 같은 인터페이스를 구현하는 게 중요

  • 각 concrete 핸들러는 execute 메서드를 가진 다음 핸들러만 신경 씀 → 코드를 concrete 클래스들에 결합하지 않고 다양한 핸들러를 사용해 런타임에 체인을 구성할 수 있게 됨

구조

1. 핸들러 - 모든 concrete 핸들러들에게 공통되는 인터페이스 선언
   - 주로 요청을 처리하는 단일 메서드만을 가지지만, 가끔 체인의 다음 핸들러를 설정하는 메서드를 가질 수도 있음
    
2. (optional) 기초 핸들러 - 모든 핸들러 클래스들에게 공통된 boilerplate 코드를 넣을 수 있음
   - 주로 다음 핸들러에 대한 참조를 저장하는 필드를 정의
   - 클라이언트는 핸들러를 이전 핸들러의 생성자 또는 setter에게 전달해 체인을 만들 수 있음
   - 다음 핸들러의 존재 여부를 확인한 후 다음 핸들러로 실행을 넘기는 디폴트 핸들링 행동을 구현할 수도 있음
    
3. concrete 핸들러 - 요청을 처리하는 실제 코드를 포함
   - 각 핸들러는 요청을 받으면 처리할 것인지, 그리고 체인을 따라 요청을 전달할 것인지 결정해야 함
    
4. 클라이언트 - 애플리케이션의 로직에 따라 체인을 단 한 번 구성할 수도 있고 동적으로 구성할 수도 있음
   - 요청은 체인의 첫번째 핸들러 뿐만 아니라 어떤 핸들러로도 전송될 수 있음

적용

프로그램이 여러 종류의 요청들을 다양하게 처리해야 하지만, 정확한 요청의 종류와 순서를 미리 알 수 없는 경우

- 책임 연쇄 패턴은 여러 핸들러를 체인으로 연결해, 요청을 받으면 각 핸들러에게 요청을 처리할 수 있는지 물음 
  → 모든 핸들러들에게 요청을 처리할 기회가 주어짐

여러 핸들러를 특정 순서로 실행해야 하는 경우

- 핸들러를 어떤 순서로든 연결할 수 있기 때문에, 모든 요청은 계획된 대로 체인을 지남

핸들러들의 집합과 순서가 런타임에 변경되는 경우

- 핸들러 클래스들 내부 참조 필드에 setters를 제공하면 핸들러들을 동적으로 삽입, 삭제, 재배열할 수 있음

구현방법

1. 핸들러 인터페이스를 선언하고 요청 처리 메서드의 시그니처 설명
   - 클라이언트가 어떻게 메서드에 데이터를 전달할 것인지 결정 
   → 가장 유연한 방법은 요청을 객체로 변환해 핸들링 메서드에 매개변수로 전달하는 것
   
2. concrete 핸들러들의 중복된 boilerplate 코드를 없애기 위해, 
   핸들러 인터페이스로부터 파생된 추상 기초 핸들러 클래스 생성 고려
   - 해당 클래스는 체인의 다음 핸들러에 대한 참조를 저장하는 필드 필요 (불변 클래스 고려)
   - 체인을 런타임에 변경하려면 참조 필드의 값을 변경하는 setter를 정의해야 함
   - 다음 객체가 있으면 요청을 다음으로 보내는 디폴트 핸들링 메서드를 구현할 수도 있음 
     → concrete 핸들러들이 부모 메서드를 호출함으로써 사용할 수 있게 됨
    
3. 핸들러 서브클래스들을 만들고 핸들링 메서드를 구현, 각 핸들러는 요청을 받으면 두 가지 결정을 해야 함
   - 요청을 처리할 것인지
   - 체인을 따라 요청을 전달할 것인지
    
4. 클라이언트는 직접 체인을 조립할 수도, 다른 객체로부터 미리 만들어진 체인을 받을 수도 있음
   - 후자라면 설정된 환경에 따라 체인을 만드는 팩토리 클래스들을 구현해야 함
    
5. 클라이언트는 첫번째 핸들러 뿐만 아니라 체인에 있는 어떤 핸들러든 사용 가능
   - 요청은 체인의 끝에 도달하거나 핸들러가 더 이상 요청을 전달하지 않을 때까지 체인을 따라 전달됨
    
6. 체인의 동적 특성으로 인해 클라이언트는 다음 시나리오를 대비해야 함:
   - 체인이 단일 링크로 구성될 수 있음
   - 요청이 체인 끝에 도달하지 않을 수 있음
   - 요청이 처리되지 않은 상태로 체인 끝에 도달할 수 있음

장단점

장점

- 요청 처리 순서를 제어할 수 있음
- SRP - 행동을 실행하는 클래스들과 행동을 유발하는 클래스들을 분리할 수 있음
- OCP - 기존 클라이언트 코드를 훼손하지 않고 새 핸들러를 도입할 수 있음

단점

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

다른 패턴과의 관계

- 책임 연쇄, 커맨드, 중재자, 옵저버 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다룸
  - 책임 연쇄 패턴 - 요청이 처리될 때까지 잠재적 수신자들로 구성된 동적 체인을 따라 전달함
  - 커맨드 - 수신자와 발신자 단방향 커넥션 수립
  - 중재자 - 수신자와 발신자 사이의 직접적인 연결을 제거하고 중재자 객체를 통해서만 소통하게 함
  - 옵저버 - 수신자들이 동적으로 요청 수신을 구독/구독 취소할 수 있음
   
- 책임 연쇄 패턴 - 종종 컴포지트 패턴과 함께 사용
  - 리프 요소가 요청을 받으면, 객체 트리의 루트까지 연쇄적으로 전달
  
- 책임 연쇄 패턴의 핸들러들은 커맨드 패턴으로 구현 가능 
  → 다양한 작업들을 같은 컨텍스트 객체에 대해 실행할 수 있고, 해당 객체는 요청으로 표현됨
  - 요청 자체가 커맨드 객체인 경우 → 같은 연산을 체인으로 연결된 서로 다른 일련의 콘텍스트들에서 실행할 수 있음
  
- 책임 연쇄 패턴과 데코레이터 패턴은 비슷한 클래스 구조를 가지지만 차이점들 존재
  - 둘 다 재귀적 합성에 의존해 일련의 객체에 실행 전달
  - 책임 연쇄 핸들러 - 독립적으로 임의의 작업을 실행할 수 있고, 아무 때나 요청 전달을 멈출 수 있음
  - 데코레이터 - 기본 인터페이스를 유지하며 객체의 행위를 확장할 수 있고, 요청의 흐름을 끊을 수 없음

TypeScript 예제

/**
 * The Handler interface declares a method for building the chain of handlers.
 * It also declares a method for executing a request.
 */
interface Handler {
    setNext(handler: Handler): Handler;

    handle(request: string): string;
}

/**
 * The default chaining behavior can be implemented inside a base handler class.
 */
abstract class AbstractHandler implements Handler
{
    private nextHandler: Handler;

    public setNext(handler: Handler): Handler {
        this.nextHandler = handler;
        // Returning a handler from here will let us link handlers in a
        // convenient way like this:
        // monkey.setNext(squirrel).setNext(dog);
        return handler;
    }

    public handle(request: string): string {
        if (this.nextHandler) {
            return this.nextHandler.handle(request);
        }

        return null;
    }
}

/**
 * All Concrete Handlers either handle a request or pass it to the next handler
 * in the chain.
 */
class MonkeyHandler extends AbstractHandler {
    public handle(request: string): string {
        if (request === 'Banana') {
            return `Monkey: I'll eat the ${request}.`;
        }
        return super.handle(request);

    }
}

class SquirrelHandler extends AbstractHandler {
    public handle(request: string): string {
        if (request === 'Nut') {
            return `Squirrel: I'll eat the ${request}.`;
        }
        return super.handle(request);
    }
}

class DogHandler extends AbstractHandler {
    public handle(request: string): string {
        if (request === 'MeatBall') {
            return `Dog: I'll eat the ${request}.`;
        }
        return super.handle(request);
    }
}

/**
 * The client code is usually suited to work with a single handler. In most
 * cases, it is not even aware that the handler is part of a chain.
 */
function clientCode(handler: Handler) {
    const foods = ['Nut', 'Banana', 'Cup of coffee'];

    for (const food of foods) {
        console.log(`Client: Who wants a ${food}?`);

        const result = handler.handle(food);
        if (result) {
            console.log(`  ${result}`);
        } else {
            console.log(`  ${food} was left untouched.`);
        }
    }
}

/**
 * The other part of the client code constructs the actual chain.
 */
const monkey = new MonkeyHandler();
const squirrel = new SquirrelHandler();
const dog = new DogHandler();

monkey.setNext(squirrel).setNext(dog);

/**
 * The client should be able to send a request to any handler, not just the
 * first one in the chain.
 */
console.log('Chain: Monkey > Squirrel > Dog\n');
clientCode(monkey);
console.log('');

console.log('Subchain: Squirrel > Dog\n');
clientCode(squirrel);
// Output.txt

Chain: Monkey > Squirrel > Dog

Client: Who wants a Nut?
  Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
  Monkey: I'll eat the Banana.
Client: Who wants a Cup of coffee?
  Cup of coffee was left untouched.

Subchain: Squirrel > Dog

Client: Who wants a Nut?
  Squirrel: I'll eat the Nut.
Client: Who wants a Banana?
  Banana was left untouched.
Client: Who wants a Cup of coffee?
  Cup of coffee was left untouched.

참고 자료: Refactoring.guru

0개의 댓글