Chain of Responsibility[Design Pattern]

SnowCat·2023년 3월 16일
0

Design Pattern

목록 보기
14/23
post-thumbnail

의도

책임 연쇄 패턴 -> 핸들러들의 체인을 따라 요청을 전달할 수 있게 해주는 행동 디자인 패턴

#문제

  • 온라인 주문 시스템을 개발하고 있다 가정해보자. 인증된 사용자들만 주문을 생성할 수 있고, 관리 권한이 있는 사용자들은 모든 주문에 대한 전체 접근 권한을 가질 수 있어야 한다.
  • 처음에는 두개의 인증 권한만이 있었지만, 나중에 분기 조건들이 추가되게 되면 조건 검증이 매우 복잡해질 우려가 있다.

해결책

  • 각각의 행동들을 핸들러라는 독립 실행 객체로 변환해 실행
  • 각각의 검사는 검사를 수행하는 메서드가 있는 자체 클래스로 추출해야 함
  • 책임 연쇄 패턴을 사용해 각각의 핸들러들을 체인 형식으로 연결
  • 핸들러 사용 시 체인을 진행하다 요청을 중단시킬 수 있다는 장점을 가질 수 있음
  • 모든 핸들러들은 같은 인터페이스를 구현해야 함

구조

// 핸들러 인터페이스
interface Handler {
    setNext(handler: Handler): Handler;

    handle(request: string): string;
}

/**
 * 핸들러 인터페이스를 따르는 기초 핸들러 클래스
 * 다음 핸들러에 대한 참조를 저장하는 레퍼런스와 setter를 가짐
 * handle 메서드에서 기본적인 행동을 정의하고 다음 핸들러에 요청을 넘겨주게 됨
 */
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 === '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);
    }
}

// 클라이언트 코드
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.`);
        }
    }
}

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);
/*
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.
*/

console.log('');

console.log('Subchain: Squirrel > Dog\n');
clientCode(squirrel);
/*
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.
*/

적용

  • 프로그램이 다양한 방식의 여러 요청을 처리하지만, 정확한 요청 유형과 순서를 미리 알 수 없는 경우에 사용
    여러 핸들러를 하나로 연결해주고, 각 핸들러에게 요청을 처리할 수 있는지 질문해 모든 핸들러가 요청을 처리할 수 있도록 해줌
  • 특정 순서로 여러 핸들러들을 실행해야 할 때 사용
    순서를 명시해 요청을 처리할 수 있음
  • 핸들러들의 집합과 순서가 런타임에 변경될 필요가 있을 때 사용
    핸들러 클래스 내부 참조 필드의 setter를 사용해 핸들러를 동적으로 삽입, 제거할 수 있음

구현 방법

  1. 핸들러 인터페이스를 선언하고 요청을 처리하는 메서드의 시그니처 선언
    클라이언트가 요청 데이터를 메서드에 전달하는 방법을 결정해야 함
  2. 구상 핸들러에서 코드 중복을 방지하려면 핸들러들의 상위 추상 클래스를 만드는 것도 고려할 필요가 있음
    추상 클래스에는 다음 체인에 대한 핸들러 레퍼런스, (변경이 필요한 경우) 레퍼런스를 변경하는 setter, 핸들링 메서드의 기본 행동을 정의해야 함
  3. 구상 핸들러 클래스를 만들고 각각의 처리 메서드를 구현함, 이 때 요청을 처리할지, 체인에 다른 요청을 넘겨줄지 여부를 결정해야 함
  4. 클라이언트는 체인을 조립하거나 받아 객체에 요청을 전달함
    단, 체인은 단방향으로 구성되고, 체인 중간에 요청이 드랍되거나, 일부 요청이 처리되지 않을 수 있음을 고려해야 함

장단점

  • 요청의 처리 순서를 제어할 수 있음
  • 작업을 호출하는 클래스와 작업을 수행하는 클래스를 분리해 단일 책임 원칙 준수
  • 새로운 핸들러를 도입할 때 클라이언트 코드 수정이 없기 때문에 개방, 폐쇄 원칙 준수
  • 일부 요청이 처리되지 않을 수 있음

출처:
https://refactoring.guru/ko/design-patterns/chain-of-responsibility

profile
냐아아아아아아아아앙

0개의 댓글