Circuit Breaker가 왜 필요한가

외부 API 호출과 같은 remote call시, 호출 실패나 hang 등을 고려하지 않을 수 없습니다. 일시적이고 단발성인 오류는 적절히 timeout을 주고 오류를 try-catch 하면 되지만, 오류가 장시간 계속 발생할 때는 이런 방식으로 해결할 수 없는 경우가 발생합니다. 응답을 받지 못한 request가 timeout이 되는 시간까지 쓰레드풀이나 DB풀을 선점하고 있거나, 메모리를 잡아 먹으면서 점차 리소스는 부족해지고, 같은 리소스를 사용하고 있는 다른 부분들에도 순식간에 장애가 전파되기 시작합니다.

그렇다면 이런 문제를 해결하기 위해서는 어떻게 해야 될까요? 문제가 되는 두가지 조건 중 하나를 없애면 됩니다.
첫번째는, 오류가 전파되지 않도록 공유하고 있는 리소스를 분리하는 방법이고,
두번째는, 오류 발생시 오랫동안 리소스를 잡아두지 못하게 하는 방법입니다.
Circuit Breaker는 여기서 두번째 방법에 해당됩니다.

Circuit Breaker 패턴

Circuit breaker 패턴은 Release It에서 처음 소개된 패턴이고, 전기의 회로차단기에서 차용한 개념입니다. 회로가 close될때는 정상적으로 전기가 흐르다가 문제가 생기면 회로를 open하여 더이상 전기가 흐르지 않도록 한 것과 같이, 평소(Close state)에는 정상적으로 동작하다가, 오류 발생시(Open state) 더이상 동작하지 않도록 합니다. 이렇게 문제가 되는 기능 자체를 동작하지 않게 해서 리소스를 점유하지 않게 하는 겁니다.
open_closed.jpg
물론 전류가 복구되면 다시 정상화 되는 회로차단기처럼, 기능이 복구되면 다시 서비스를 정상화시켜야 하는데요. 일정 시간이 지났다고 무작정 정상 상태(Close state)로 돌리면, request가 갑자기 몰리면서 문제가 다시 발생하겠죠. 그래서 일부 request만 실행해보면서 기능이 다시 정상적으로 동작하는지 확인하는 과정(Half open state)이 필요합니다. 그렇게 정상화되었다고 판단되면 다시 원래 상태로 복구하게 됩니다.
여기서 총 3가지 상태가 언급되었는데요. 정리해보면 아래와 같습니다.

정상 상태 : Close
오류 상태 : Open
반열림 상태 : Half Open

3가지 상태간의 전환은 아래와 같이 됩니다.
state.png

Circuit Breaker 구현

Circuit Breaker를 간단하게 구현해봅시다.Martin fowler의 Circuit Breaker의 코드를 조금 수정해서 javascript로 구현할께요. 먼저 3가지 상태를 정의합니다.

const State = Object.freeze({
  Close : Symbol('close'),
  Open : Symbol('open'),
  HalfOpen : Symbol('half-open')
});

특정 기능을 wrapping하여 Close상태이면 정상적으로 동작하고, Open 상태이면 바로 error를 반환하는 함수를 추가합니다. Half Open인 경우 간단하게 랜덤하게 request를 실행할 수 있게 해봅시다.

call(command) {
  switch (this.state()) {
    case State.HalfOpen :
      if (this.executeByRandom()) {
        try {
          command();
          this.resetFailure();
        } catch (e) {
          // Continue Error
        }
      } else {
          throw new Error("Error");
      }
      break;
    case State.Close :
      try {
        command();
      } catch (e) {
        this.recordFailure();
        throw e;
      }
      break;
    case State.Open :
      throw new Error("Error");
    default :
      throw new Error("Unknown State");
  }
}

마지막으로 상태를 반환하는 기능만이 남았습니다. 10번 이상 오류가 발생하면 Open 상태가 되고, 1분마다 정상이 되었는지 확인하기 위해 Half Open 상태를 반환하는 함수를 만듭니다.

state() {
    if (this.failureCount >= 10) {
      if ((new Date().getTime() - this.lastFailureTime) > 1000 * 60){
        return State.HalfOpen;
      } else {
        return State.Open;
      }
    } else {
      return State.Close;
    }
  }

전체 소스는 여기에서 확인하실 수 있습니다.

참고