출처 : 디자인 패턴에 뛰어들기 - 알렉산더 슈베츠 도서
책임 연쇄 패턴(CoR, Chain of Responsiblity)
핸들러들의 체인(사슬)을 따라 요청을 전달할 수 있게 해주는 행동 디자인 패턴이다.
각 핸들러는 요청을 받으면 요청을 처리할지 아니면 체인의 다음 핸들러로 전달할지를 결정한다.
문제
당신이 온라인 주문 시스템을 개발하고 있다고 가정해본다.
- 당신은 인증된 사용자들만 주문을 생성할 수 있도록 시스템에 대한 접근을 제한하려고 한다.
- 또 관리 권한이 있는 사용자들에게는 모든 주문에 대한 전체 접근 권한을 부여하려고 한다.
- 당신은 약간의 설계 후에 이러한 검사들은 차례대로 수행해야 한다는 사실을 깨닫게 된다.
- 당신의 앱은 사용자들의 자격 증명이 포함된 요청을 받을 때마다 시스템에 대해 사용자 인증을 시도할 수 있다.
- 그러나 이러한 자격 증명이 올바르지 않아서 인증에 실패하면 다른 검사들을 진행할 이유가 없다.
요청은 주문 시스템 자체가 처리할 수 있기 전에 일련의 검사들을 통과해야 한다는 예시이다.
다음 몇 달 동안 당신은 이러한 순차 검사들을 몇 가지 더 구현한다.
동료 중 한 명이 검증되지 않은 데이터를 주문 시스템에 직접 전달하는 것은 안전하지 않다고 제안한다.
- 그래서 당신은 요청 내의 데이터를 정제(sanitize)하는 추가 유효성 검사 단계를 추가한다.
- 그래서 당신은 요청 내의 데이터를 정제(sanitize)하는 추가 유효성 검사 단계를 추가한다.
나중에 누군가가 시스템이 무차별 대입 공격에 취약하다는 사실을 발견했으며, 이러한 공격을 방어하기 위해 같은 IP 주소에서 오는 반복적으로 실패한 요청을 걸러내는 검사를 즉시 추가한다.
또 다른 누군가는 같은 데이터가 포함된 반복 요청에 대해 캐시된 결과를 반환하여 시스템 속도를 높일 수 있다고 제안했고, 당신은 적절한 캐시 응답이 없는 경우에만 요청이 시스템으로 전달되도록 하는 또 다른 검사를 추가한다.
코드가 커질수록 더 복잡해진다는 것을 알 수 있는 예시이다.
이미 엉망진창이었던 검사 코드는 당신이 새로운 기능을 추가할 때마다 더욱 크게 부풀어 오를 것이다. 하나의 검사 코드를 바꾸면 다른 검사 코드가 영향을 받기도 했을 것이다.
더 심각한 문제는, 시스템의 다른 컴포넌트들을 보호하기 위해 검사를 재사용하려고 할 때 해당 컴포넌트들에 일부 코드를 복제해야 했다는 것이다.
왜냐하면 컴포넌트들이 필요로 한 것은 검사의 일부였지, 모든 검사는 아니었기 때문이다.
해당 시스템은 이해하기가 매우 어려웠고 유지 관리 비용이 많이 들었으며, 당신은 프로그램 전체를 리팩토링하기로 할 때까지 한동안 코드와 씨름해야할 것으로 보여진다.
해결책
다른 여러 행동 디자인 패턴들과 마찬가지로 책임 연쇄 패턴은 특정 행동들을 핸들러라는 독립 실행형 객체들로 변환한다.
- 자신이 만든 앱의 경우 각 검사는 검사를 수행하는 단일 메서드가 있는 자체 클래스로 추출되어야 한다.
- 이제 요청은 데이터와 함께 이 메서드에 인수로 전달된다.
이 패턴은 이러한 핸들러들을 체인으로 연결하도록 제안한다.
- 연결된 각 핸들러에는 체인의 다음 핸들러에 대한 참조를 저장하기 위한 필드가 있다.
- 요청을 처리하는 것 외에도 핸들러들은 체인을 따라 요청을 더 멀리 전달한다.
- 이 요청은 모든 핸들러가 요청을 처리할 기회를 가질 때까지 체인을 따라 이동한다.
가장 좋은 부분은
핸들러가 요청을 체인 아래로 더 이상 전달하지 않고, 추가 처리를 사실상 중지하는 결정을 내릴 수 있다는 것이다.
당신의 주문 관리 시스템에서는 하나의 핸들러가 주문 처리를 수행한 다음 요청을 체인 아래로 더 전달할지를 결정한다.
요청에 올바른 데이터가 포함되어 있다고 가정하면 모든 핸들러들은 인증 확인이든 캐싱이든 그들의 주 행동들을 실행할 수 있다.
핸들러들이 하나씩 줄지어 체인을 형성하는 예시이다.
한편, 약간 다른 조금 더 정식적인 접근 방법이 있다.
이 방식에서는 핸들러가 요청을 받으면 핸들러는 요청을 처리할 수 있는지를 판단한다.
처리가 가능한 경우
- 핸들러는 이 요청을 더 이상 전달하지 않습니다. 따라서 요청을 처리하는 핸들러는 하나뿐이거나 아무 핸들러도 요청을 처리하지 않는다.
- 이 접근 방식은 그래픽 사용자 인터페이스 내에서 요소들의 스택에서 이벤트들을 처리할 때 매우 일반적이다.
- 예를 들어,
- 사용자가 버튼을 클릭하면 결과 이벤트는 그래픽 사용자 인터페이스 요소 체인을 통해 전파된다.
- 이 체인은 버튼으로 시작하여 해당 컨테이너들(예: 양식 또는 패널)을 따라 이동한 후 메인 애플리케이션 창으로 끝난다.
- 또 이 이벤트는 그를 처리할 수 있는 체인의 첫 번째 요소에 의해 처리된다.
- 즉, 해당 예는 체인이 항상 객체 트리에서 추출될 수 있음을 보여주기 때문이다.
체인은 객체 트리의 가지에서부터 형성될 수 있는 예시이다.
모든 핸들러 클래스들이 같은 인터페이스를 구현하는 것은 매우 중요하다.
- 각 구상 핸들러는 execute 메서드가 있는 다음 핸들러에만 신경을 써야 한다.
- 다양한 핸들러들을 사용하여 코드를 핸들러들의 구상 클래스들에 결합하지 않고도 런타임에 체인들을 구성할 수 있다.
실제상황 적용
기술 지원 부서로의 전화는 여러 교환원을 거쳐 이루어질 수 있다.
위 예시로 상황 설명은 해본다.
당신은 당신의 컴퓨터에 새 하드웨어를 구매하여 설치했다.
당신은 컴퓨터 괴짜이기 때문에 당신의 컴퓨터에는 여러 운영 체제가 설치되어 있다.
당신은 이 하드웨어가 지원되는지 확인하기 위해 모든 운영 체제들의 부팅을 시도한다.
윈도우는 하드웨어를 자동으로 감지하고 활성화한다.
그러나 당신이 애지중지하는 리눅스 운영 체제는 새 하드웨어와 작업하는 것을 거부한다.
당신은 약간의 희망을 품고 상자에 적힌 기술 지원 전화번호로 전화하기로 한다.
가장 먼저 들리는 것은 자동 응답기의 로봇 음성이다.
- 이 음성은 다양한 문제에 대한 9가지 인기 있는 솔루션을 제안하지만, 그 중 어느 것도 당신의 문제와 관련이 없다.
잠시 후 로봇이 당신을 실제 교환원에게 연결하게 된다. - 그러나 이 교환원도 별로 도움이 될만한 제안을 하지 않는다.
- 그는 당신의 문제를 경청하지 않은 채 사용자 설명서에서 발췌한 긴 문장을 계속 인용한다.
- '컴퓨터를 껐다가 다시 켜 보셨습니까?' 같은 별 쓸모없는 문구를 10번 이상 들은 후, 당신은 적절한 엔지니어와 연결해 줄 것을 요구한다.
- 이 음성은 다양한 문제에 대한 9가지 인기 있는 솔루션을 제안하지만, 그 중 어느 것도 당신의 문제와 관련이 없다.
결국 드디어 교환원은 어둡고 외로운 지하 서버실에서 인간적인 접촉을 갈망했던 엔지니어 중 한 명에게 전화를 연결한다.
이 엔지니어는 새 하드웨어에 적합한 드라이브를 어디에서 다운받아야 하는지와 리눅스에 설치하는 방법 등을 알려준다.
드디어 문제가 해결됨으로써 당신은 기쁜 마음으로 통화를 종료한다.
구조
1. 핸들러는 모든 구상 핸들러에 공통적인 인터페이스를 선언한다.
- 일반적으로 여기에는 요청을 처리하기 위한 단일 메서드만 포함되지만 때로는 체인의 다음 핸들러를 세팅하기 위한 다른 메서드가 있을 수도 있다.
- 일반적으로 여기에는 요청을 처리하기 위한 단일 메서드만 포함되지만 때로는 체인의 다음 핸들러를 세팅하기 위한 다른 메서드가 있을 수도 있다.
2. 기초 핸들러는 선택적 클래스이며 여기에 모든 핸들러 클래스들에 공통적인 상용구 코드를 넣을 수 있다.
- 일반적으로 이 클래스는 다음 핸들러에 대한 참조를 저장하기 위한 필드를 정의한다.
- 클라이언트들은 핸들러를 이전 핸들러의 생성자 또는 세터(setter)에 해당 핸들러를 전달하여 체인을 구축할 수 있다.
- 또 클래스는 디폴트 핸들러 행동을 구현할 수도 있다.
- 즉, 다음 핸들러의 존재 여부를 확인한 후 다음 핸들러로 실행을 넘길 수 있다.
3. 구상 핸들러들에는 요청을 처리하기 위한 실제 코드가 포함되어 있다.
- 각 핸들러는 요청을 받으면 이 요청을 처리할지와 함께 체인을 따라 전달할지를 결정해야 한다.
- 핸들러들은 일반적으로 자체 포함형이고 불변하며, 생성자를 통해 필요한 모든 데이터를 한 번만 받는다.
4. 클라이언트는 앱의 논리에 따라 체인들을 한 번만 구성하거나 동적으로 구성할 수 있다.
- 참고로 요청은 체인의 모든 핸들러에 보낼 수 있으며, 꼭 첫 번째 핸들러일 필요는 없습니다.
의사코드
이 예에서 책임 연쇄 패턴은 활성 그래픽 사용자 인터페이스 요소에 대한 상황별 도움말 정보를 표시하는 역할을 한다.
그래픽 사용자 인터페이스 클래스들은 복합체 패턴으로 빌드되고, 각 요소는 그 요소의 컨테이너 요소에 연결된다.
또 언제든지 요소 자체에서 시작하여 그 요소의 모든 컨테이너 요소를 통과하는 요소 체인을 구축할 수 있는 예시이다.
앱의 그래픽 사용자 인터페이스의 구조는 일반적으로 객체 트리로 구성된다.
- 예를 들어
- 앱의 기본 창을 렌더링하는 Dialog(대화 상자) 클래스는 객체 트리의 뿌리(root)가 된다.
- Dialog에는 Panels(패널들)가 포함되어 있다.
- 여기에는 다른 패널들이나 Buttons(버튼들) 및 TextFields(문자 필드들)와 같은 단순한 하위 설계 요소들이 포함될 수 있다.
- 간단한 컴포넌트는 컴포넌트에 어떤 도움말 텍스트가 할당되어 있는 한 상황에 맞는 짧은 도구 도움말들을 표시할 수 있다.
- 그러나 더 복잡한 컴포넌트들은 (예를 들어 설명서에서 발췌한 내용을 표시하거나 브라우저에서 웹페이지를 여는 것과 같은 상황에 맞는 도움말을 표시하는 컴포넌트들) 상황별 도움말을 나타내기 위한 그들의 고유한 방법들을 정의할 수 있다.
도움말 요청이 그래픽 사용자 인터페이스 객체들을 가로질러 이동하는 방법의 예시이다.
사용자가 요소에 마우스 커서를 놓고 F1 키를 누르면 앱은 포인터 아래에 있는 컴포넌트를 감지하고 그에게 도움 요청을 보낸다. 이 요청은 도움말 정보를 표시할 수 있는 요소에 도달할 때까지 모든 요소의 컨테이너를 통과하며 올라간다.
// 핸들러 인터페이스는 요청을 실행하기 위한 메서드를 선언합니다.
interface ComponentWithContextualHelp is
method showHelp()
// 간단한 컴포넌트들의 기초 클래스.
abstract class Component implements ComponentWithContextualHelp is
field tooltipText: string
// 컴포넌트의 컨테이너는 핸들러 체인의 다음 링크 역할을 합니다.
protected field container: Container
// 컴포넌트는 도움말 텍스트가 할당되었을 때 도구 설명을 표시합니다.
// 그렇지 않으면 컨테이너가 있는 경우 호출을 해당 컨테이너로 전달합니다.
method showHelp() is
if (tooltipText != null)
// 도구 설명 표시하기.
else
container.showHelp()
// 컨테이너는 간단한 컴포넌트들과 다른 컨테이너들을 자식으로 포함할 수 있습니다.
// 여기에서 체인 관계들이 설립됩니다. 이 클래스는 부모로부터 showHelp 행동을 상속합니다.
abstract class Container extends Component is
protected field children: array of Component
method add(child) is
children.add(child)
child.container = this
// 원시적인 컴포넌트들은 디폴트 도움말 구현으로 괜찮을 수 있습니다.
class Button extends Component is
// …
// 그러나 복잡한 컴포넌트들은 기초 구현을 오버라이드할 수 있습니다.
// 도움말 텍스트를 새로운 방식으로 제공할 수 없는 경우
// 컴포넌트는 언제든지 기초 구현을 호출할 수 있습니다. (컴포넌트 클래스 참조).
class Panel extends Container is
field modalHelpText: string
method showHelp() is
if (modalHelpText != null)
// 도움말 텍스트와 함께 모달 창을 표시합니다.
else
super.showHelp()
// …위와 같음…
class Dialog extends Container is
field wikiPageURL: string
method showHelp() is
if (wikiPageURL != null)
// 위키 도움말 페이지를 엽니다.
else
super.showHelp()
// 클라이언트 코드
class Application is
// 모든 앱은 체인을 다르게 설정합니다.
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)
// 여기에서 무슨 일이 일어날지 상상해 보세요.
method onF1KeyPress() is
component = this.getComponentAtMouseCoords()
component.showHelp()
적용
책임 연쇄 패턴은 당신의 프로그램이 다양한 방식으로 다양한 종류의 요청들을 처리할 것으로 예상되지만 정확한 요청 유형들과 순서들을 미리 알 수 없는 경우에 사용하세요.
- 이 패턴은 당신이 여러 핸들러를 하나의 체인으로 연결할 수 있도록 해준다.
- 또 요청을 받으면 바로 각 핸들러에게 이 요청을 처리할 수 있는지 질문한다.
- 이렇게 해야 모든 핸들러들은 요청을 처리할 기회를 얻는다.
이 패턴은 특정 순서로 여러 핸들러를 실행해야 할 때 사용하세요.
- 당신은 체인의 핸들러들을 원하는 순서로 연결할 수 있으므로 모든 요청은 정확히 당신이 계획한 대로 체인을 통과한다.
책임 연쇄 패턴은 핸들러들의 집합과 그들의 순서가 런타임에 변경되어야 할 때 사용하세요.
- 당신이 핸들러 클래스들 내부의 참조 필드에 세터들을 제공하면, 핸들러들을 동적으로 삽입, 제거 또는 재정렬할 수 있을 것이다.
구현방법
1. 핸들러 인터페이스를 선언하고 요청을 처리하는 메서드의 시그니처를 설명하세요.
- 클라이언트가 요청 데이터를 메서드에 전달하는 방법을 결정해야 한다.
- 가장 유연한 방법은 요청을 객체로 변환하여 처리 메서드에 인수로 전달하는 것이다.
2. 구상 핸들러들에서 중복된 상용구 코드를 제거하려면 핸들러 인터페이스에서 파생된 추상 기초 핸들러 클래스를 만드는 것도 고려해볼 만하다.
- 이 클래스에는 체인의 다음 핸들러에 대한 참조를 저장하기 위한 필드가 있어야 한다.
- 이 클래스를 불변으로 만드는 것을 고려해야 한다.
- 그러나 런타임에 체인들을 수정할 계획이라면 참조 필드의 값을 변경하기 위한 세터(setter)를 정의해야 한다.
- 또 처리(핸들링) 메서드를 위한 편리한 디폴트(기본값) 행동을 구현할 수 있다.
- 이 행동은 남아있는 객체가 없을 때까지 요청을 다음 객체로 넘기는 것이다.
- 구상 핸들러들은 부모 메서드를 호출하여 이 행동을 사용할 수 있다.
3. 하나씩 구상 핸들러 자식 클래스들을 만들고 그들의 처리 메서드들을 구현하세요.
- 각 핸들러는 요청을 받았을 때 두 가지 결정을 내려야 한다.
- 요청을 처리할지의 여부.
- 체인을 따라 요청을 전달할지의 여부.
- 각 핸들러는 요청을 받았을 때 두 가지 결정을 내려야 한다.
4. 클라이언트는 자체적으로 체인을 조립하거나 다른 객체들에서부터 미리 구축된 체인을 받을 수 있다.
- 후자의 경우 설정 또는 환경 설정에 따라 체인들을 구축하기 위해 일부 공장 클래스들을 구현해야 한다.
- 후자의 경우 설정 또는 환경 설정에 따라 체인들을 구축하기 위해 일부 공장 클래스들을 구현해야 한다.
5. 클라이언트는 첫 번째 핸들러뿐만 아니라 체인의 모든 핸들러를 활성화할 수 있다.
- 요청은 어떤 핸들러가 더 이상의 전달을 거부하거나 요청이 체인 끝에 도달할 때까지 체인을 따라 전달된다.
- 요청은 어떤 핸들러가 더 이상의 전달을 거부하거나 요청이 체인 끝에 도달할 때까지 체인을 따라 전달된다.
6. 체인의 동적 특성으로 인해 클라이언트는 다음 상황들을 처리할 준비가 되어 있어야 한다.
- 체인은 단일 링크로 구성될 수 있습니다.
- 일부 요청들은 체인 끝에 도달하지 못할 수 있습니다.
- 다른 요청들은 처리되지 않은 상태로 체인의 끝에 도달할 수 있습니다.
장단점
장점
- 요청의 처리 순서를 제어할 수 있다.
- 단일 책임 원칙.
- 작업을 호출하는 클래스들을 작업을 수행하는 클래스들과 분리할 수 있다.
- 개방/폐쇄 원칙.
*기존 클라이언트 코드를 손상하지 않고 앱에 새 핸들러들을 도입할 수 있다.
단점
- 일부 요청들은 처리되지 않을 수 있다.
다른 패턴과의 관계
커맨드, 중재자, 옵서버 및 책임 연쇄 패턴은 요청의 발신자와 수신자를 연결하는 다양한 방법을 다루고 있다.
- 책임 연쇄 패턴은 잠재적 수신자의 동적 체인을 따라 수신자 중 하나에 의해 요청이 처리될 때까지 요청을 순차적으로 전달한다.
- 커맨드 패턴은 발신자와 수신자 간의 단방향 연결을 설립한다.
- 중재자 패턴은 발신자와 수신자 간의 직접 연결을 제거하여 그들이 중재자 객체를 통해 간접적으로 통신하도록 강제한다.
- 옵서버 패턴은 수신자들이 요청들의 수신을 동적으로 구독 및 구독 취소할 수 있도록 한다.
책임 연쇄 패턴은 종종 복합체 패턴과 함께 사용된다.
- 잎 컴포넌트가 요청을 받으면 해당 요청을 모든 부모 컴포넌트들의 체인을 통해 객체 트리의 뿌리(root)까지 전달할 수 있다.
책임 연쇄 패턴의 핸들러들은 커맨드로 구현할 수 있다.
- 많은 다양한 작업을 같은 콘텍스트 객체에 대해 실행할 수 있다.
- 해당 콘텍스트 객체는 요청의 역할을 합니다. 여기에서의 요청은 처리 메서드의 매개변수를 의미한다.
- 그러나 요청 자체가 커맨드 객체인 다른 접근 방식이 있다.
- 이 접근 방식을 사용하면 당신은 같은 작업을 체인에 연결된 일련의 서로 다른 콘텍스트들에서 실행할 수 있다.
책임 연쇄 패턴과 데코레이터는 클래스 구조가 매우 유사하다.
두 패턴 모두 실행을 일련의 객체들을 통해 전달할 때 재귀적인 합성에 의존하나, 몇 가지 결정적인 차이점이 있다.
책임 연쇄 패턴 핸들러
*서로 독립적으로 임의의 작업을 실행할 수 있다.- 또한 해당 요청을 언제든지 더 이상 전달하지 않을 수 있다.
- 또한 해당 요청을 언제든지 더 이상 전달하지 않을 수 있다.
다양한 데코레이터
- 객체의 행동을 확장한다.
- 동시에 이러한 행동을 기초 인터페이스와 일관되게 유지할 수 있다.
- 또한 데코레이터들은 요청의 흐름을 중단할 수 없다.
'Self-Dev > Design Patterns R&D' 카테고리의 다른 글
[27] 디자인 패턴 목록 - 행동 디자인 패턴 - 반복자 패턴(Iterator Patten) (0) | 2024.07.13 |
---|---|
[26] 디자인 패턴 목록 - 행동 디자인 패턴 - 커멘드 패턴(Command Patten) (0) | 2024.07.12 |
[24] 디자인 패턴 목록 - 구조 패턴 - 프록시 패턴(Proxy Patten) (0) | 2024.07.07 |
[23] 디자인 패턴 목록 - 구조 패턴 - 플라이웨이트 패턴(Flyweight Pattern or Cash Pattten) (0) | 2024.07.06 |
[22] 디자인 패턴 목록 - 구조 패턴 - 퍼사드 패턴(Facade Pattern) (0) | 2024.07.05 |