Resilience4j 란?
- fault tolerance (결함 허용) 라이브러리이다. 즉 오류나 장애가 발생해도 서비스가 중단되지 않게 안정적으로 도와주는 서비스이다.
- 이 기능을 구현하기 위한 여러 Resilience4j 모듈이 있다.
- 장애가 전파되지 않도록 차단하는 CircuitBreaker
- 실패 시 자동 재시작으로 장애 극복을 시도하는 Retry
- 결과를 캐시해 장애가 나더라도 결과를 전달하게 하는 Cacjhe
- 갑작스럽게 트래픽이 폭증한 상황에서 요청량을 제어하는 RateLimiter
- 자원을 배분해서 장애가 나도 다른 서비스에서 작동하도록 하는 BulkHead
- 응답시간 초과 시 타임아웃으로 지연 장애가 전파되지 않도록 하는 TimeLimiter
이 포스팅에서는 Circuit Breaker 모듈에 집중해서 알아보도록 하겠다.
Circuit Breaker란?
- Finite State Machine (유한 상태 기계) 개념을 사용해 구현되었다. 즉 정해져있는 세가지의 상태에 따라 입력을 처리하고 다음 상태로 전환한다.
- CLOSED, OPEN, HALF_OPEN 세가지 상태가 기본이고 특수한 상황에 METRICS_ONLY, DISABLED, FORCED_OPEN 상태를 가지고 있다.

다른 상태로 전환하는 전이 규칙은 아래와 같다.
- CLOSED → OPEN (실패율 초과): 요청이 여러번 실패하거나 지연이 너무 긴 경우 차단기가 실행됨 (OPEN)
- 상태의 변화는 일정한 Threshold를 기준으로 한다. 실패율이나 지연율이 Threshold 이상이라면 실패라고 간주하고 OPEN 상태로 전환한다. 실패율의 경우 실패라고 간주할 수 있는 예외의 목록을 따로 설정할 수 있다.
- OPEN → HALF_OPEN (타이머 만료 후 시험 호출): 일정 시간이 지난 뒤 제한적으로 호출을 허용함 (HALF_OPEN)
- 차단기가 실행된 이후 서버가 활성화되어있는 지 시험해보기 위해 설정한 만큼의 요청을 테스트로 받아보는 상태로 전환한다.
- HALF_OPEN → CLOSED (시험 성공) / OPEN (시험 실패): 제한적으로 호출을 허용하여 테스트한 뒤 요청이 성공하면 차단기를 끄고 (CLOSED) 실패하면 차단기를 다시 실행한다 (OPEN)
그렇다면 Circuit Breaker가 어떻게 장애 상태를 파악할 수 있을까?
실패율과 지연율을 측정하기 위해서는 요청의 결과를 지속적으로 수집하고 관찰해야한다. Circuit Breaker에서는 이를 위해 두 가지 방식을 사용한다.
Count-based Sliding Window
최근 N개의 요청의 결과를 모은다. 윈도우 크기가 10이라면 요청의 개수는 항상 10개이다.
요청이 추가될 때마다 최근에 유입된 요청의 결괏값이 더해지고, 가장 오래된 요청의 결괏값이 빼진다.
이 시간복잡도는 O(1)이고 공간복잡도는 O(N)이다.
Time-based Sliding Window
최근 N초의 요청 결과를 모은다. 1초를 하나의 '버킷'이라는 개념으로 생각하고, 한 버킷마다 생긴 요청의 결과를 저장해둔다. 예를 들어 윈도우 크기가 10이라면 각 1초 동안 생긴 요청의 결과를 각 버킷에 저장한다.
bucket[0] = 3 success / 1 fail
bucket[1] = 1 success / 0 fail
bucket[2] = 2 success / 1 fail
...
모든 요청이 개별적으로 저장되는 것이 아니라, 버킷마다 성공과 실패된 요청의 합계가 저장된다.
그래서 윈도우가 이동할 때마다 (1초 경과할 때마다) 가장 최근의 버킷의 결과를 더하고, 가장 오래된 버킷의 결과를 빼면 되므로 효율적으로 시간 기반의 윈도우를 개발할 수 있다.
여러 스레드에서 Circuit Breaker의 상태를 변경한다면?
Circuit Breaker는 하나의 객체로 힙 메모리에서 단 하나의 상태를 가진다. 하지만 여러 스레드가 JVM에서 동작하면서 Circuit Breaker의 상태를 동시에 변경할 수 있다. 이 위험을 방지하기 위해서 Circuit Breaker는 기본적으로 Thread-safe한 방식으로 구현되었다.
- 상태는 AtomicReference에 저장된다.
- 원자적 연산 (Atomic Operations)를 사용해 상태를 갱신한다.
- 슬라이딩 윈도우에서 요청을 읽고 계산하는 작업은 Synchronized 상태에서 진행된다.
이에 따라 슬라이딩 윈도우의 변경사항을 읽고 상태를 갱신하는 건 하나의 스레드에서만 가능하다.
하지만 애플리케이션 함수 호출 자체를 동기화하지 않는다. Circuit Breaker에 따른 큰 성능 저하를 막기 위해서이다.
함수 호출 자체를 동기화하기 위해서는 BulkHead를 사용할 수 있다.

이 사진은 세 개의 스레드가 동시에 CircuitBreaker 에 접근해서 함수호출을 시도한 경우이다. 복잡해보이지만, 아래 두가지 프로세스에 기반해서 이해해볼 수 있다.
- 스레드가 Circuit Breaker를 호출했을 때 CLOSED 상태인 경우 true를 스레드에게 반환하고, 스레드는 Protected function을 호출한다.
- Protected function에서 예외가 발생한 경우 스레드에 예외가 반환되고, 스레드는 onError(exception) 함수를 통해 CircuitBreaker에 오류가 발생한 사실을 알린다.
- 예외가 일정횟수를 넘기면 CallNotPermittedException가 발생되고 CircuitBreaker가 OPEN 상태로 전환된다. 이 경우 스레드가 CircuitBreaker를 통해 Protected function에 접근을 요청해도 예외를 반환한다.
저 다이어그램이 시사하는 점은 아래와 같다.
- 여러 스레드가 동시에 Circuit Breaker를 통해 protected function을 호출할 수 있다. 즉, protected function을 호출하는 것은 동기화되지 않는다.
- T2의 요청이 진행 중인 사이에 T1에 오류가 생겨서 임계값이 도달하였다. T2의 요청이 여전히 진행 중임에도 불구하고 T3이 요청하였을 때 임계값 도달에 의해서 바로 CallNotPermittedException 예외를 던진다. 이에 따라 스레드가 protected function을 동기화해서 호출하지는 않지만 예외를 집계하고 상태를 전환하는 것은 atomic하게 진행된다는 것을 알 수 있다.
다음에는 OpenFeign에서 Resilience4J를 사용하는 방식에 대해서 알아보겠다.