
이전 포스팅에 이어 식구하자 프로젝트에서 마이크로서비스 아키텍처(MSA)의 안정성을 강화하기 위해 Gateway 서버에 Resilience4j를 활용하여 서킷브레이커를 적용하는 과정을 소개하고자 합니다.
위에 그림처럼 저희가 서비스가 운영하는 과정에서 앞으로 발생할 수있는 예외를 안다면 더할 나위없이 좋겠지만 , 현실은 그렇게 녹록치 않습니다,,,,.
특히 MSA 환경에서는 특정 서버의 장애가 다른 서버로 전파될 수 있는 위험이 있습니다. 이러한 문제는 CircuitBreaker 패턴을 사용하여 효과적으로 해결할 수 있습니다.
기존에 마이크로서비스 간 동기 호출 시 CircuitBreaker 패턴을 적용하여 개별 서비스 수준에서 장애 전파를 방지했지만, 전체 시스템 차원의 중앙 집중식 장애 전파 방지 대책은 부재했습니다. 서킷 브레이커를 게이트웨이 서버에 적용함으로써 중앙에서 트래픽을 제어할 수 있게 되어, 서비스의 전반적인 안정성을 크게 향상시킬 수 있습니다.
이 포스팅에서는 Spring Cloud Gateway와 Resilience4j를 활용하여 이러한 중앙 집중식 서킷 브레이커 패턴을 구현하는 과정을 설명해보도록 하겠습니다!
CircuitBreaker는 마이크로서비스 아키텍처(MSA)에서 중요한 역할을 하는 장애 관리 패턴입니다 (유사 전기 회로의 차단기).
Circuit Breaker가 필요한 이유는, 누전 차단기가 전기 사고가 발생하기 전에 전기를 미리 차단하는 것과 동일하게, 문제가 있는 마이크로서비스로의 트래픽을 차단하여 전체 서비스가 느려지거나 중단되는 것을 미리 방지합니다.

멤버 서비스가 채팅 서비스를 호출하는 상황을 가정해봅시다. 만약 쿠폰 서비스가 지속적으로 실패한다면, CircuitBreaker는 '열린(Open)' 상태가 되어 쿠폰 서비스로의 호출을 차단합니다. 이를 통해:
CircuitBreaker의 주요 상태:
이러한 메커니즘을 통해 CircuitBreaker는 시스템의 안정성을 높이고, 장애 상황에서도 전체 시스템의 가용성을 유지하는 데 중요한 역할을 합니다.
Circuit Breaker의 동작 원리는 다음과 같습니다:
Circuit Breaker는 호출 결과를 저장하고 분석하기 위해 슬라이딩 윈도우를 사용합니다.횟수 기반: 최근 N번의 호출 결과를 기록시간 기반: 최근 N초 동안의 호출 결과를 기록CallNotPermittedException 발생AtomicReference에 저장되어 동시성 문제 방지atomic 연산으로 처리서킷브레이커를 사용하기 위해서는 의존성을 추가해야 합니다. 서킷브레이커를 구현하고자 하는 시스템의 구현에 따라서 서로 다른 의존성을 제공하며, Getting Started 페이지에서 확인할 수 있습니다!
저는 아래 의존성을 추가했습니다:
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j'
spring:
cloud:
gateway:
default-filters:
- name: CircuitBreaker
args:
name: defaultCircuitBreaker
fallbackUri: forward:/fallback
resilience4j:
circuitbreaker:
configs:
default:
register-health-indicator: true # 서킷 브레이커의 상태를 헬스 인디케이터로 등록
allow-health-indicator-to-fail: false # 헬스 인디케이터가 실패할 수 있는지 여부
sliding-window-type: COUNT_BASED # 슬라이딩 윈도우 타입: COUNT_BASED는 호출 수를 기준으로 함
sliding-window-size: 10 # 슬라이딩 윈도우 크기: 10개의 호출
minimum-number-of-calls: 10 # 서킷 브레이커가 동작하기 위한 최소 호출 수
failure-rate-threshold: 50 # 실패율 임계값 (%)
slow-call-rate-threshold: 50 # 느린 호출 비율 임계값 (%)
slow-call-duration-threshold: 10s # 느린 호출의 기준 시간 (초)
wait-duration-in-open-state: 10s # 서킷 브레이커가 오픈 상태에서 유지되는 시간 (초)
automatic-transition-from-open-to-half-open-enabled: false # 오픈 상태에서 반 오픈 상태로 자동 전환 여부
permitted-number-of-calls-in-half-open-state: 5 # 반 오픈 상태에서 허용되는 호출 수
record-exceptions: # 서킷 브레이커가 예외로 간주할 예외 클래스들
- java.util.concurrent.TimeoutException # 타임아웃 예외
- org.springframework.cloud.gateway.support.NotFoundException # NotFound 예외
- io.github.resilience4j.circuitbreaker.CallNotPermittedException # 서킷 브레이커가 호출을 허용하지 않는 예외
instances:
defaultCircuitBreaker:
baseConfig: default # 기본 설정을 상속받음
failure-rate-threshold: 50 # 실패율 임계값을 50%로 설정
서킷브레이커의 주요 설정값은 아래와 같습니다:
failureRateThreshold: 호출 실패율에 대한 임계값, 슬라이딩 윈도우 상에서 임곗값보다 실패율이 높아지면 상태가 OPEN 됩니다. 기본값은 50입니다.slidingWindowSize: 슬라이딩 윈도우의 크기를 설정합니다. 기본값은 100입니다.slidingWindowType: 슬라이딩 윈도의 형태를 COUNT_BASED로 설정할지, TIME_BASED로 설정할지 결정합니다. 타입에 따라 slidingWindowSize의 숫자가 의미하는 것이 호출 횟수 또는 시간이 됩니다. 기본값은 COUNT_BASED입니다.waitDurationInOpenState: OPEN상태에서HALF_OPEN` 상태로의 전환을 기다리는 시간을 설정합니다. 기본값은 60초입니다.
automaticTransitionFromOpenToHalfOpenEnabled: OPEN 상태에서 자동으로 HALF_OPEN 상태로 전환할지 여부를 설정합니다. 기본값은 true입니다.추가로)
CircuitBreaker의 instances 섹션에서는 여러 개의 CircuitBreaker 인스턴스를 정의할 수 있습니다. 각 인스턴스는 서로 다른 서비스나 API 엔드포인트에 대해 독립적으로 설정될 수 있습니다.
예를 들어:
instances:
defaultCircuitBreaker:
baseConfig: default
failure-rate-threshold: 50
serviceACircuitBreaker:
baseConfig: default
failure-rate-threshold: 30
serviceBCircuitBreaker:
baseConfig: default
failure-rate-threshold: 40
이렇게 설정하면, 각 서비스나 API에 대해 다른 CircuitBreaker 설정을 적용할 수 있습니다. 서비스의 특성, 중요도, 예상되는 부하 등에 따라 각 인스턴스의 파라미터를 조정할 수 있습니다.
이를 통해 각 서비스에 맞는 최적화된 CircuitBreaker 동작을 구현할 수 있으며, 시스템 전체의 안정성과 회복력을 향상시킬 수 있습니다.
[서버URL]/actuator/circuitbreakers 요청을 하면 등록된 서킷브레이커를 확인할 수 있습니다!

위에 yml을 설정을 보시면
서킷브레이커의 fallback 동작은 서비스 장애 시 사용자에게 대체 응답을 제공하는 메커니즘입니다. YAML 설정의 fallbackUri: forward:/fallback은 문제 발생 시 요청을 /fallback 엔드포인트로 보내도록 지시합니다.
이 /fallback 엔드포인트는 컨트롤러에서 처리됩니다.
@RequestMapping("/fallback")
public Mono<ResponseEntity<String>> fallback(ServerWebExchange exchange) {
Throwable exception = exchange.getAttribute(ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR);
log.info("예외: {}", exception != null ? exception.getMessage() : "Unknown error");
return Mono.just(exception)
.map(ex -> {
if (ex instanceof NotFoundException) {
return new ResponseEntity<>("내부 서버 오류가 발생했습니다. 관리자에게 문의해 주세요.", HttpStatus.SERVICE_UNAVAILABLE);
} else if (ex instanceof TimeoutException) {
return new ResponseEntity<>("서비스 요청 시간이 초과되었습니다. 나중에 다시 시도해 주세요.", HttpStatus.GATEWAY_TIMEOUT);
} else if (ex instanceof CallNotPermittedException) {
return new ResponseEntity<>("서비스가 현재 사용 불가능합니다. 잠시 후 다시 시도해 주세요.", HttpStatus.SERVICE_UNAVAILABLE);
} else {
return new ResponseEntity<>("내부 서버 오류가 발생했습니다. 관리자에게 문의해 주세요.", HttpStatus.SERVICE_UNAVAILABLE);
}
})
.defaultIfEmpty(new ResponseEntity<>("내부 서버 오류가 발생했습니다. 관리자에게 문의해 주세요.", HttpStatus.SERVICE_UNAVAILABLE));
}
fallback method는 요청 실패에 대한 Exception별로 구분해서 작성할 수 있습니다!
ServerWebExchange에서 CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR 속성을 통해 발생한 예외를 가져옵니다.NotFoundException: 내부 서버 오류 메시지 반환TimeoutException: 서비스 요청 시간 초과 메시지 반환CallNotPermittedException: 서비스 사용 불가 메시지 반환ResponseEntity로 감싸 반환합니다.아래 테이블은 CircuitBreaker/Retry 설정에 사용되는 속성값과 그에 대한 설명입니다.
CircuitBreaker property
| property | description |
|---|---|
| failureRateThreshold | 실패 비율 임계치를 백분율로 설정 해당 값을 넘어갈 시 Circuit Breaker 는 Open 상태로 전환되며, 이때부터 호출을 차단한다 (기본값: 50) |
| slowCallRateThreshold | 임계값을 백분율로 설정, CircuitBreaker는 호출에 걸리는 시간이 slowCallDurationThreshold보다 길면 느린 호출로 간주,해당 값을 넘어갈 시 Circuit Breaker 는 Open상태로 전환되며, 이때부터 호출을 차단한다 (기본값: 100) |
| slowCallDurationThreshold | 호출에 소요되는 시간이 설정한 임계치보다 길면 느린 호출로 계산.응답시간이 느린 것으로 판단할 기준 시간 (60초, 1000 ms = 1 sec) (기본값: 60000[ms]) |
| permittedNumberOfCallsInHalfOpenState | HALF_OPEN 상태일 때, OPEN/CLOSE 여부를 판단하기 위해 허용할 호출 횟수를 설정 수 (기본값: 10) |
| maxWaitDurationInHalfOpenState | HALF_OPEN 상태로 있을 수 있는 최대 시간이다. 0일 때 허용 횟수만큼 호출을 모두 완료할 때까지 HALF_OEPN 상태로 무한정 기다린다. (기본값: 0) |
| slidingWindowType | sliding window 타입을 결정한다. COUNT_BASED인 경우 slidingWindowSize 만큼의 마지막 call들이 기록되고 집계된다.TIME_BASED인 경우 마지막 slidingWindowSize초 동안의 call들이 기록되고 집계됩니다. (기본값: COUNT_BASED) |
| slidingWindowSize | CLOSED 상태에서 집계되는 슬라이딩 윈도우 크기를 설정한다. (기본값: 100) |
| minimumNumberOfCalls | minimumNumberOfCalls 이상의 요청이 있을 때부터 faiure/slowCall rate를 계산.예를 들어, 해당 값이 10이라면 최소한 호출을 10번을 기록해야 실패 비율을 계산할 수 있다.기록한 호출 횟수가 9번뿐이라면 9번 모두 실패했더라도 circuitbreaker는 열리지 않는다. (기본값: 100) |
| waitDurationInOpenState | OPEN에서 HALF_OPEN 상태로 전환하기 전 기다리는 시간 (60초, 1000 ms = 1 sec) (기본값: 60000[ms]) |
| recordExceptions | 실패로 기록할 Exception 리스트 (기본값: empty) |
| ignoreExceptions | 실패나 성공으로 기록하지 않을 Exception 리스트 (기본값: empty) |
| ignoreException | 기록하지 않을 Exception을 판단하는 Predicate을 설정 (커스터마이징, 기본값: throwable -> true) |
| recordFailure | 어떠한 경우에 Failure Count를 증가시킬지 Predicate를 정의해 CircuitBreaker에 대한 Exception Handler를 재정의.true를 return할 경우, failure count를 증가시키게 된다 (기본값: false) |
Fallback 처리가 정상적으로 동작하는지, 특정 마이크로서비스를 일부로 다운시킨 후 호출하고 내려오는 응답을 확인해보도록 하겠습니다

Spring Cloud Gateway에서 Resilience4j를 사용하여 서킷 브레이커 패턴을 적용하고, 서킷 브레이커의 상태 변화를 로그로 기록하려면 Resilience4j의 이벤트를 구독하여 상태 변화 시 로그를 남길 수 있습니다!!
Resilience4j는 서킷 브레이커의 상태 변화나 호출 이벤트를 구독할 수 있는 이벤트 리스너를 제공합니다. 이를 통해 서킷 브레이커의 상태 변화가 발생할 때마다 로그를 기록할 수 있습니다. 아래는 상태 변화와 관련된 이벤트를 구독하여 로그를 남기는 코드 예제입니다.
@Bean
public RegistryEventConsumer<CircuitBreaker> circuitBreakerEventConsumer() {
return new RegistryEventConsumer<CircuitBreaker>() {
@Override
public void onEntryAddedEvent(EntryAddedEvent<CircuitBreaker> entryAddedEvent) {
CircuitBreaker circuitBreaker = entryAddedEvent.getAddedEntry();
circuitBreaker.getEventPublisher()
.onFailureRateExceeded(event -> log.error("Circuit breaker '{}' failure rate {}% exceeded at {}",
event.getCircuitBreakerName(), event.getFailureRate(), event.getCreationTime()))
.onError(event -> log.error("Circuit breaker '{}' encountered error, duration: {}s",
event.getCircuitBreakerName(), event.getElapsedDuration().getSeconds()))
.onStateTransition(event -> log.warn("Circuit breaker '{}' state transition from '{}' to '{}' at {}",
event.getCircuitBreakerName(), event.getStateTransition().getFromState(),
event.getStateTransition().getToState(), event.getCreationTime()));
}
@Override
public void onEntryRemovedEvent(EntryRemovedEvent<CircuitBreaker> entryRemoveEvent) {
log.debug("Circuit breaker '{}' removed", entryRemoveEvent.getRemovedEntry().getName());
}
@Override
public void onEntryReplacedEvent(EntryReplacedEvent<CircuitBreaker> entryReplacedEvent) {
log.debug("Circuit breaker '{}' replaced", entryReplacedEvent.getNewEntry().getName());
}
};
}
onEntryAddedEvent: 서킷 브레이커가 추가될 때 호출됩니다. 이 이벤트를 통해 서킷 브레이커의 상태 변화, 실패율 초과, 오류 발생 등의 정보를 로그로 남길 수 있습니다.onEntryRemovedEvent: 서킷 브레이커가 제거될 때 호출됩니다. 서킷 브레이커의 제거 정보를 로그로 기록합니다.onEntryReplacedEvent: 서킷 브레이커가 교체될 때 호출됩니다. 교체된 서킷 브레이커의 정보를 로그로 기록합니다.
식구 하자 프로젝트에서 장애 전파를 방지하기 위해 Spring Cloud Gateway에 Resilience4j를 이용한 Circuit Breaker 패턴을 적용했습니다. 이 Circuit Breaker의 상태를 actuator를 통해 확인할 수 있지만 매번 일일이 확인하기엔 굉장히 귀찮고 번거로움이 있습니다..
효과적으로 모니터링하고 시각화하기 위해 아래처럼 Grafana를 활용하여 상태를 확인할 수 있습니다
(해당 포스팅에서는 prometheus와 Grafana 적용 과정에 대해 다루지 않습니다!)

이번 포스팅을 끝으로 Spring Cloud Gateway 패턴에서 장애 대응 하는 방법을 살펴보았습니다. 모든 시스템에는 장애가 발생할 가능성이 있으며, 이를 효과적으로 대응하기 위한 조치는 단순히 개발자가 연동할 시스템에 대한 신뢰 문제나 과도한 설계가 아닙니다. 이는 장애 전파를 방지하기 위한 기본적인 방어 수단입니다.
그렇다면 MSA는 장애가 발생하면 큰일 나는 아키텍처일까요?
버너 보겔스는 "모든 소프트웨어는 결국 실패할 수 있다"고 말했습니다. 이 말에는 중요한 교훈이 숨어 있습니다.
"완벽한 시스템을 만드는 것보다, 실패에 유연하고 빠르게 대응할 수 있는 시스템을 설계하는 것이 중요하다."
이러한 관점에서 볼 때, MSA 아키텍처는 장애에 대한 내성을 높이고, 장애 발생 시 신속하게 대응할 수 있는 시스템을 구축하는 데 적합한 접근 방식입니다. 장애 대응 전략과 도구를 적절히 활용하면, 시스템의 안정성과 신뢰성을 크게 향상시킬 수 있습니다.
이 포스팅이 여러분의 MSA 아키텍처 구축과 장애 대응에 도움이 되길 바랍니다!
오늘도 읽어주셔서 감사합니다 ☺️
https://techblog.woowahan.com/15694/
https://medium.com/@im_zero/spring-cloud-gateway-circuit-breaker-time-limiter-5e3c26a62b4c
https://bkjeon1614.tistory.com/712
https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#spring-cloud-feign-circuitbreaker
좋은 글 감사합니다!