디자인 패턴

Chain of Responsibility 패턴

Keisa 2024. 7. 25. 10:32

 의도

책임 연쇄 패턴은 핸들러들의 체인​(사슬)​을 따라 요청을 전달할 수 있게 해주는 행동 디자인 패턴입니다. 각 핸들러는 요청을 받으면 요청을 처리할지 아니면 체인의 다음 핸들러로 전달할지를 결정합니다.

 문제

당신이 온라인 주문 시스템을 개발하고 있다고 가정해봅시다. 당신은 인증된 사용자들만 주문을 생성할 수 있도록 시스템에 대한 접근을 제한하려고 합니다. 또 관리 권한이 있는 사용자들에게는 모든 주문에 대한 전체 접근 권한을 부여하려고 합니다.

당신은 약간의 설계 후에 이러한 검사들은 차례대로 수행해야 한다는 사실을 깨달았습니다. 당신의 앱은 사용자들의 자격 증명이 포함된 요청을 받을 때마다 시스템에 대해 사용자 인증을 시도할 수 있습니다. 그러나 이러한 자격 증명이 올바르지 않아서 인증에 실패하면 다른 검사들을 진행할 이유가 없습니다.

요청은 주문 시스템 자체가 처리할 수 있기 전에 일련의 검사들을 통과해야 합니다.

다음 몇 달 동안 당신은 이러한 순차 검사들을 몇 가지 더 구현했습니다.

  • 동료 중 한 명이 검증되지 않은 데이터를 주문 시스템에 직접 전달하는 것은 안전하지 않다고 제안했습니다. 그래서 당신은 요청 내의 데이터를 정제​(sanitize)​하는 추가 유효성 검사 단계를 추가했습니다.
  • 나중에 누군가가 시스템이 무차별 대입 공격에 취약하다는 사실을 발견했으며, 이러한 공격을 방어하기 위해 같은 IP 주소에서 오는 반복적으로 실패한 요청을 걸러내는 검사를 즉시 추가했습니다.
  • 또 다른 누군가는 같은 데이터가 포함된 반복 요청에 대해 캐시된 결과를 반환하여 시스템 속도를 높일 수 있다고 제안했고, 당신은 적절한 캐시 응답이 없는 경우에만 요청이 시스템으로 전달되도록 하는 또 다른 검사를 추가했습니다.

코드가 커질수록 더 복잡해졌습니다.

이미 엉망진창이었던 검사 코드는 당신이 새로운 기능을 추가할 때마다 더욱 크게 부풀어 올랐습니다. 하나의 검사 코드를 바꾸면 다른 검사 코드가 영향을 받기도 했습니다. 더 심각한 문제는, 시스템의 다른 컴포넌트들을 보호하기 위해 검사를 재사용하려고 할 때 해당 컴포넌트들에 일부 코드를 복제해야 했다는 것입니다. 왜냐하면 컴포넌트들이 필요로 한 것은 검사의 일부였지, 모든 검사는 아니었기 때문입니다.

당신의 시스템은 이해하기가 매우 어려웠고 유지 관리 비용이 많이 들었으며, 당신은 프로그램 전체를 리팩토링하기로 할 때까지 한동안 코드와 씨름했습니다.

 해결책

다른 여러 행동 디자인 패턴들과 마찬가지로 책임 연쇄 패턴은 특정 행동들을 라는 독립 실행형 객체들로 변환합니다. 당신의 앱의 경우 각 검사는 검사를 수행하는 단일 메서드가 있는 자체 클래스로 추출되어야 합니다. 이제 요청은 데이터와 함께 이 메서드에 인수로 전달됩니다.

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

가장 좋은 부분은 핸들러가 요청을 체인 아래로 더 이상 전달하지 않고 추가 처리를 사실상 중지하는 결정을 내릴 수 있다는 것입니다.

당신의 주문 관리 시스템에서는 하나의 핸들러가 주문 처리를 수행한 다음 요청을 체인 아래로 더 전달할지를 결정합니다. 요청에 올바른 데이터가 포함되어 있다고 가정하면 모든 핸들러들은 인증 확인이든 캐싱이든 그들의 주 행동들을 실행할 수 있습니다.

핸들러들이 하나씩 줄지어 체인을 형성합니다.

한편, 약간 다른 조금 더 정식적인 접근 방법이 있습니다. 이 방식에서는 핸들러가 요청을 받으면 핸들러는 요청을 처리할 수 있는지를 판단합니다. 처리가 가능한 경우, 핸들러는 이 요청을 더 이상 전달하지 않습니다. 따라서 요청을 처리하는 핸들러는 하나뿐이거나 아무 핸들러도 요청을 처리하지 않습니다. 이 접근 방식은 그래픽 사용자 인터페이스 내에서 요소들의 스택에서 이벤트들을 처리할 때 매우 일반적입니다.

예를 들어, 사용자가 버튼을 클릭하면 결과 이벤트는 그래픽 사용자 인터페이스 요소 체인을 통해 전파됩니다. 이 체인은 버튼으로 시작하여 해당 컨테이너들​(예: 양식 또는 패널)​을 따라 이동한 후 메인 애플리케이션 창으로 끝납니다. 또 이 이벤트는 그를 처리할 수 있는 체인의 첫 번째 요소에 의해 처리됩니다. 이 예가 주목할 만한 이유는 체인이 항상 객체 트리에서 추출될 수 있음을 보여주기 때문입니다.

체인은 객체 트리의 가지에서부터 형성될 수 있습니다.

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

 실제상황 적용

기술 지원 부서로의 전화는 여러 교환원을 거쳐 이루어질 수 있습니다.

당신은 당신의 컴퓨터에 새 하드웨어를 구매하여 설치했습니다. 당신은 컴퓨터 괴짜이기 때문에 당신의 컴퓨터에는 여러 운영 체제가 설치되어 있습니다. 당신은 이 하드웨어가 지원되는지 확인하기 위해 모든 운영 체제들의 부팅을 시도합니다. 윈도우는 하드웨어를 자동으로 감지하고 활성화합니다. 그러나 당신이 애지중지하는 리눅스 운영 체제는 새 하드웨어와 작업하는 것을 거부합니다. 당신은 약간의 희망을 품고 상자에 적힌 기술 지원 전화번호로 전화하기로 합니다.

가장 먼저 들리는 것은 자동 응답기의 로봇 음성입니다. 이 음성은 다양한 문제에 대한 9가지 인기 있는 솔루션을 제안하지만, 그 중 어느 것도 당신의 문제와 관련이 없습니다. 잠시 후 로봇이 당신을 실제 교환원에게 연결합니다.

그러나 이 교환원도 별로 도움이 될만한 제안을 하지 않습니다. 그는 당신의 문제를 경청하지 않은 채 사용자 설명서에서 발췌한 긴 문장을 계속 인용합니다. '컴퓨터를 껐다가 다시 켜 보셨습니까?' 같은 별 쓸모없는 문구를 10번 이상 들은 후, 당신은 적절한 엔지니어와 연결해 줄 것을 요구합니다.

결국 드디어 교환원은 어둡고 외로운 지하 서버실에서 인간적인 접촉을 갈망했던 엔지니어 중 한 명에게 전화를 연결합니다. 이 엔지니어는 새 하드웨어에 적합한 드라이브를 어디에서 다운받아야 하는지와 리눅스에 설치하는 방법 등을 알려줍니다. 드디어 문제가 해결되었군요! 당신은 기쁜 마음으로 통화를 종료합니다.

 

/**
 * The Handler interface declares a method for building the chain of handlers.
 * It also declares a method for executing a request.
 */
class Handler {
 public:
  virtual Handler *SetNext(Handler *handler) = 0;
  virtual std::string Handle(std::string request) = 0;
};
/**
 * The default chaining behavior can be implemented inside a base handler class.
 */
class AbstractHandler : public Handler {
  /**
   * @var Handler
   */
 private:
  Handler *next_handler_;

 public:
  AbstractHandler() : next_handler_(nullptr) {
  }
  Handler *SetNext(Handler *handler) override {
    this->next_handler_ = handler;
    // Returning a handler from here will let us link handlers in a convenient
    // way like this:
    // $monkey->setNext($squirrel)->setNext($dog);
    return handler;
  }
  std::string Handle(std::string request) override {
    if (this->next_handler_) {
      return this->next_handler_->Handle(request);
    }

    return {};
  }
};
/**
 * All Concrete Handlers either handle a request or pass it to the next handler
 * in the chain.
 */
class MonkeyHandler : public AbstractHandler {
 public:
  std::string Handle(std::string request) override {
    if (request == "Banana") {
      return "Monkey: I'll eat the " + request + ".\n";
    } else {
      return AbstractHandler::Handle(request);
    }
  }
};
class SquirrelHandler : public AbstractHandler {
 public:
  std::string Handle(std::string request) override {
    if (request == "Nut") {
      return "Squirrel: I'll eat the " + request + ".\n";
    } else {
      return AbstractHandler::Handle(request);
    }
  }
};
class DogHandler : public AbstractHandler {
 public:
  std::string Handle(std::string request) override {
    if (request == "MeatBall") {
      return "Dog: I'll eat the " + request + ".\n";
    } else {
      return AbstractHandler::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.
 */
void ClientCode(Handler &handler) {
  std::vector<std::string> food = {"Nut", "Banana", "Cup of coffee"};
  for (const std::string &f : food) {
    std::cout << "Client: Who wants a " << f << "?\n";
    const std::string result = handler.Handle(f);
    if (!result.empty()) {
      std::cout << "  " << result;
    } else {
      std::cout << "  " << f << " was left untouched.\n";
    }
  }
}
/**
 * The other part of the client code constructs the actual chain.
 */
int main() {
  MonkeyHandler *monkey = new MonkeyHandler;
  SquirrelHandler *squirrel = new SquirrelHandler;
  DogHandler *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.
   */
  std::cout << "Chain: Monkey > Squirrel > Dog\n\n";
  ClientCode(*monkey);
  std::cout << "\n";
  std::cout << "Subchain: Squirrel > Dog\n\n";
  ClientCode(*squirrel);

  delete monkey;
  delete squirrel;
  delete dog;

  return 0;
}