Behavioral patterns은 효과적인 의사소통과 객체 간의 책임 할당을 담당한다.
책임 사슬은 핸들러 체인을 따라 요청을 전달할 수 있는 동작 설계 패턴이다. 요청을 수신하면 각 핸들러는 요청을 처리할지 체인의 다음 핸들러에게 전달할지를 결정한다.
이 패턴을 통해 여러 객체가 수신기의 특정 클래스의 송신자 클래스를 결합하지 않고 요청을 처리할 수 있다. 체인은 표준 핸들러 인터페이스를 따르는 어떤 핸들러로도 런타임에 동적으로 구성될 수 있다.
온라인 주문 시스템에서 일하고 있다고 상상해보자. 인증된 사용자만 주문을 할 수 있도록 시스템에 대한 액세스를 제한하려고 한다. 또한 관리 권한이 있는 사용자는 모든 주문에 대한 전체 액세스 권한이 있어야 한다.
이를 위해 로직 설계를 생각해보았을 때, 검사를 순차적으로 수행하여 자격 증명이 올바른 경우와 올바르지 않은 경우로 나누어 더 이상 진행을 할지 말지 결정할 수 있을 것이다.
주문 시스템 자체가 요청을 처리할 수 있으려면 먼저 일련의 검사를 통과해야 한다.
인증이외에 안전을 위한 추가 검증 단계, 반복적인 실패 요청을 필터링하는 검사, 반복요청에 대한 캐싱 검사를 추가했다고 가정해보자.
코드가 커질수록 더 복잡해진다.
각각의 새로운 기능을 추가함에 따라 코드의 크기는 더 커질 것이다. 검증 하나를 바꾸는 것이 다른 검증에도 영향을 미치고 이를 해결하기 위해 코드 일부를 복제하여 다시 쓰는 등의 작업이 발생함에 따라 시스템이 점차 난해해 질 것이다.
다른 많은 행동 디자인 패턴과 마찬가지로 책임 사슬은 특정한 행동을 핸들러라고 불리는 독립 객체로 변환하는 것에 의존한다. 각 검사는 검사를 수행하는 단일 방법으로 자체 클래스로 추출되어야 한다. 요청은 데이터와 함께 이 메서드에 인수로 전달된다.
이 패턴을 보면 이러한 처리기를 체인으로 연결할 수 있다. 연결된 각 핸들러에는 체인의 다음 핸들러에 대한 참조를 저장하는 필드가 있다. 요청을 처리하는 것 외에도 핸들러는 체인을 따라 요청을 전달하고, 모든 핸들러가 요청을 처리할 수 있을 때까지 요청은 체인을 따라 이동한다.
여기서 더 좋은 점은 핸들러가 요청을 필요한 경우에만 하고 필요 없는 경우 중지할 수 있다는 점이다.
주문 시스템의 예에서 핸들러는 처리를 수행한다음 요청을 체인 아래로 전달할지 여부를 결정한다.
핸들러들이 한 줄씩 줄을 서서 체인을 이룬다.
그러나 요청을 받았을 때 핸들러가 처리할 수 있는지 여부를 결정하는 다른 접근법이 있다. 그것이 가능하다면 더이상 요청을 전달하지 않는데 이 접근 방식은 GUI내의 요소 스택에서 이벤트를 처리할 때 매우 일반적이다.
예를 들어, 사용자가 버튼을 클릭하면 이벤트는 버튼에서 시작하여 컨테이너를 따라 메인 애플리케이션 창으로 이어지는 GUI요소의 체인을 통해 전파된다. 이벤트는 체인에서 처리할 수 있는 첫 번째 요소에 의해 처리된다. 이 예는 체인이 항상 객체 트리에서 추출될 수 있음을 보여준다.
체인은 객체 트리의 가지에서 형성될 수 있다.
모든 핸들러 클래스가 동일한 인터페이스를 구현하는 것이 중요하다. 각각의 구체적인 핸들러는 execute
메서드가 있는 다음의 핸들러에만 신경 써야 한다. 이렇게 하면 코드를 구체적인 클래스에 결합하지 않고 다양한 핸들러를 사용하여 런타임에 체인을 구성할 수 있다.
기술 지원에 대한 호출을 여러 교환원을 통해 이루어 질 수 있다.
너가 컴퓨터를 사고 여러 운영체제를 설치하였다고 가정해보자. 근데 윈도우는 부팅이 되는데 리눅스는 부팅이 안되어서 기술지원팀에 연락을 한 상황이다.
그럼 다음의 순서로 기술지원이 진행될 것이다.
자동 응답기 → 교환원 → 기술 엔지니어
최종적으로 엔지니어를 통해 컴퓨터에 적합한 드라이버와 리눅스 설치하는 방법에 대해서 지원을 받을 수 있을 것이다.
핸들러는 인터페이스를 선언하며 모든 구체적 처리기에 공통이다. 일반적으로 요청을 처리하는 단일 메서드만 포함하지만 체인의 다음 핸들러를 설정하는 다른 메서드가 있을 수도 있다.
기본 핸들러는 모든 처리기 클래스에 공통되는 상용구 코드를 입력할 수 있는 선택적 클래스이다.
일반적으로 이 클래스는 다음 핸들러에 대한 참조를 저장하기 위한 필드를 정의한다. 클라이언트는 핸들러를 통해 이전 핸들러의 생성자나 setter에 전달하여 체인을 작성할 수 있다. 클래스는 기본 처리 동작을 구현할 수 있다. 클래스 존재 여부를 확인한 후 다음 핸들러로 실행을 전달할 수 있다.
구체적인 핸들러에는 요청을 처리하기 위한 실제 코드가 포함되어 있다. 요청을 받으면, 각 핸들러는 이를 처리할 것인지, 추가로 체인을 통해 전달할 것인지 결정해야 한다.
핸들러는 일반적으로 자급자족하며 불변하며 생성자를 통해 필요한 모든 데이터를 한 번만 수락한다.
클라이언트는 애플리케이션의 논리에 따라 체인을 한 번만 구성하거나 동적으로 구성할 수 있다. 요청은 체인의 모든 핸들러에게 전송될 수 있다.
프로그램이 다양한 방법으로 다양한 종류의 요청을 처리할 것으로 예상되지만 정확한 요청 유형과 해당 시퀀스를 미리 알 수 없는 경우 책임 사슬 패턴을 사용하라.
특정 순서로 여러 핸들러를 실행해야 할 때 패턴을 사용하라.
런타임에 핸들러 집합과 핸들러 순서가 변경되어야 하는 경우 CoR 패턴을 사용하라.
요청 처리 순서 제어 가능
SRP(단일 책임 원칙). 작업을 호출하는 클래스를 작업을 수행하는 클래스에서 분리할 수 있다.
OCP(열기/닫기 원리). 기존 클라이언트 코드를 해제하지 않고 새로운 핸들러를 앱에 도입할 수 있다.
복잡도: ★★☆
인기: ★☆☆
사용 예: 책임 사슬 패턴은 코드가 객체의 체인으로 작동할 때만 관련되어 TS에선 자주 사용하지 않는다.
식별: 이 패턴은 다른 객체에서 간접적으로 동일한 메서드를 호출하는 한 객체 그룹의 행동 메서드로 인식될 수 있지만 모든 객체는 공통 인터페이스를 따른다.
index.ts
// 핸들러 인터페이스는 핸들러들의 사슬을 구성하는 메서드를 선언한다.
interface Handler {
setNext(handler: Handler): Handler;
handle(request: string): string;
}
// 초기 사슬 행동이 기본 핸들러 클래스의 안에서 구현되어질 수 있다.
abstract class AbstractHandler implements Handler {
private nextHandler: Handler;
public setNext(handler: Handler): Handler {
this.nextHandler = handler;
return handler;
}
public handle(request: string): string {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}
// 모든 구체적인 핸들러는 체인안의 다음 핸들러로 요청 또는 통과한다.
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 === 'Nuts') {
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);
}
}
function clientCode(handler: Handler) {
const foods = ['Nuts', '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.`);
}
}
}
const monkey = new MonkeyHandler();
const squirrel = new SquirrelHandler();
const dog = new DogHandler();
monkey.setNext(squirrel).setNext(dog);
console.log('Chain: Monkey > Squirrel > Dog\n');
clientCode(monkey);
console.log('');
console.log('Subchain: Squirrel > Dog\n');
clientCode(squirrel);
결과
책임 사슬은 핸들러를 이용하여 체인을 따라 요청을 전달하여 처리하는 동작 설계 패턴이다.
→ 모든 핸들러 클래스가 동일한 인터페이스를 구현하는 것이 중요하다.