Typescript로 다시 쓰는 GoF - Chain of Responsibility

아홉번째태양·2023년 9월 25일
0

Chain of Responsibility란?

책임 떠넘기기란 말 그대로 각 객체가 본인이 처리하지 못하는 업무를 다른 객체에게 처리할 것을 떠넘기는 것을 의미한다.

Express로 만든 API 서버에서 어떤 요청이 들어왔을 때 해당 요청을 어느 엔드포인트에서 처리할지 적합한 처리 주체를 만날때까지 미들웨어와 라우터 스택을 따라 내려가는 것을 생각하면 된다. 다만, 여기서는 보다 객체 지향적으로 코드를 다루며, 스택이 아닌 체인의 형태로 책임이 전가된다.

왜 사용할까?

"떠 넘긴다"는 표현 때문에 자칫 부정적으로 들릴 수 있지만, Chain of Responsibility는 각자의 객체가 자신의 일에 집중하며, 처리를 요구하는 객체와 실제로 처리하는 객체가 느슨하게 결합된 구조일 뿐이다.

만일, 책임을 떠넘기지 않고 어느 한 주체가 어느 객체에서 처리할지 결정을 하게 된다면, 해당 객체에는 어느 객체가 어떤 처리를 할 수 있는지에 대한 모든 정보가 중앙집권적으로 강력하게 결속되어 있어야만 한다. 하지만 이는 각 객체의 독립성이 훼손되며, 요구 객체가 처리 객체들의 세부적인 역할까지 알아야 한다.

구현

구조는 굉장히 단순하며, 크게 다음의 두 가지 객체가 필요하다.

  1. Handler 처리자
    요청을 처리하는 인터페이스를 정의한다. 이때, 해당 Handler가 처리하지 못할 경우 다음에 넘길 Handler에 대한 정보를 가지고 있어야 한다.
  2. ConcreteHandler 구체적인 처리자
    Handler의 구현체이며, 각자 정해진 역할에 따라 요청을 처리한다.

Request

먼저 처리 대상이 되는 요청 객체를 간단하게 만들어두자. 각각의 요청을 식별할 수 있는 id 정도만 우선 만든다.

class Request {
  constructor(
    private readonly id: number,
  ) {}

  getId(): number {
    return this.id;
  }
}

Handler

각각의 Handler는 다음 Handler를 지목할 수 있는 메소드 setNext와 요청을 직접 처리하는 handle메소드를 갖는다.

interface Handler {
  setNext(handler: Handler): Handler;
  handle(request: Request): void;
}

abstract class AbstractHandler implements Handler {
  private next: Handler | null = null;

  constructor(
    private readonly name: string,
  ) {}

  setNext(handler: Handler): Handler {
    this.next = handler;
    return handler;
  }

  handle(request: Request): void {
    if (this.resolve(request)) {
      this.done(request);
    } else if (this.next) {
      this.next.handle(request);
    } else {
      this.fail(request);
    }
  }

  abstract resolve(request: Request): boolean;

  protected done(request: Request) {
    console.log(`${request.getId()} resolved by ${this.name}`);
  }

  protected fail(request: Request) {
    console.log(`${request.getId()} failed`);
  }
}

위 코드에서는 인터페이스를 단순화하기 위해 HandlerAbstractHandler에서 분리하였다.

AbstractHandler에서는 Template Method를 이용하여 각 요청을 어떻게 처리할지 그 흐름을 미리 작성하며, 추후 ConcreteHandler들에서 resolve 메소드에 각자의 역할에 따라 처리 가능한지 여부를 결정하도록 둔다.

ConcreteHandler

이제 적당히 여러개의 ConcreteHandler들을 만들어낸다.

class NothingHandler extends AbstractHandler {
  resolve(request: Request): boolean {
    return false;
  }
}

class LimitHandler extends AbstractHandler {
  constructor(
    name: string,
    private readonly limit: number,
  ) {
    super(name);
  }

  resolve(request: Request): boolean {
    return request.getId() < this.limit;
  }
}

class OddHandler extends AbstractHandler {
  constructor(
    name: string,
  ) {
    super(name);
  }

  resolve(request: Request): boolean {
    return request.getId() % 2 === 1;
  }
}

class SpecialHandler extends AbstractHandler {
  constructor(
    name: string,
    private readonly special: number,
  ) {
    super(name);
  }

  resolve(request: Request): boolean {
    return request.getId() === this.special;
  }
}

구현 확인

구현한 ConcreteHandler들을 인스턴스화하고, setNext 메소드를 통해 적당한 순서로 체인을 만든다.

const alice = new NothingHandler('Alice');
const bob = new LimitHandler('Bob', 10);
const charlie = new SpecialHandler('Charlie', 33);
const dave = new LimitHandler('Dave', 20);
const eve = new OddHandler('Eve');
const fred = new LimitHandler('Fred', 30);

alice
  .setNext(bob)
  .setNext(charlie)
  .setNext(dave)
  .setNext(eve)
  .setNext(fred);

위 코드에서는 요청을 alice가 처리하지 못한다면, bob, charlie, dave, eve, fred가 차례대로 처리할 수 있는 기회를 얻게 된다.

for (let i = 0; i < 50; i+=3) {
  alice.handle(new Request(i));
}
0 resolved by Bob
3 resolved by Bob
6 resolved by Bob
9 resolved by Bob
12 resolved by Dave
15 resolved by Dave
18 resolved by Dave
21 resolved by Eve
24 resolved by Fred
27 resolved by Eve
30 failed
33 resolved by Charlie
36 failed
39 resolved by Eve
42 failed
45 resolved by Eve
48 failed

참고자료

Java언어로 배우는 디자인 패턴 입문 - 쉽게 배우는 Gof의 23가지 디자인패턴 (영진닷컴)

Refactoring Guru - Chain of Responsibility

0개의 댓글