
서킷 브레이커는 마이크로서비스 간의 호출 실패를 감지해 전체 시스템 안정성을 유지하는 패턴입니다. 외부 서비스 호출 실패 시 빠른 실패를 통해 장애를 격리하고, 시스템의 다른 부분에 영향을 주지 않도록 합니다.
서킷 브레이커는 Closed, Open, Half-Open 세 가지 상태를 가지며, 각각의 상태에 따라 요청을 통과시키거나 차단합니다.
먼저 Closed 상태는 모든 요청을 통과시키는 기본 상태이며, 실패율이 임계값(예: 50% 이상)을 넘으면 Open 상태로 전환됩니다. 예를 들어 최근 5번의 호출 중 3번이 실패하여 실패율이 60%에 도달하면 오픈 상태로 전환됩니다. 즉, Closed 상태에서는 성공과 실패의 횟수를 카운트합니다.
Open 상태에서는 클라이언트가 요청을 보내면, 서킷 브레이커가 서버로 요청을 전달하지 않고 그 자리에서 즉시 거절합니다. 서버 응답을 기다리느라 5초, 10초씩 무한 로딩(타임아웃)이 걸리는 게 아니라, 0.1초 만에 바로 에러 응답을 보냅니다. 이때 단순히 에러만 던지는 게 아니라, 미리 준비한 "현재 서비스 점검 중입니다" 같은 안내 문구나 임시 데이터를 보여줄 수 있습니다. 또한 일정 대기 시간(예: 20초) 후 Half-Open 상태로 전환됩니다.
Open 상태에서는 왜 이렇게 할까요? 서버 보호와 장애 전파 방지를 해야하기 때문입니다.
이미 과부하로 비명 지르는 서버에 계속 요청을 보내면 서버가 완전히 타버릴 수 있습니다. 잠시 쉴 시간을 주는 거죠. 또한 한 서버가 느려지면 그 서버를 기다리는 다른 서비스들까지 줄줄이 대기 상태가 되어 시스템 전체가 마비됩니다. 이를 미리 끊어 전체 시스템을 살립니다.
Half-Open 상태에서는 제한된 요청만 허용해 시스템 복구 여부를 점검하며, 성공 시 Closed 로, 실패 시 Open으로 다시 전환합니다. 모든 요청을 다 보내는 게 아니라, 미리 정해둔 소수의 요청(예: 10개)만 실제 서버로 보내봅니다.
만약 요청이 성공하면 Closed(정상)로 복구합니다. 테스트 요청들이 모두 성공하면 "아, 서버가 이제 다 나았구나!"라고 판단해서 다시 길을 완전히 열어줍니다.
하나라도 실패하면 Open(차단)으로 회귀합니다. 테스트 중 에러가 발생하면 "아직 안 됐네"라고 판단하고, 즉시 다시 차단기(Open)를 내려버립니다. 그리고 다시 대기 시간을 갖습니다.
이는 관리자가 일일이 버튼을 눌러 서버를 재가동할 필요 없이, 시스템이 스스로 건강 상태를 체크하고 복구하기 위해서입니다. 또한 서버가 막 살아났는데 갑자기 수만 개의 요청이 쏟아지면 다시 죽을 수 있습니다. 그래서 조금씩 트래픽을 흘려보내며 서버를 보호하는 역할을 합니다.
Resilience4j는 서킷 브레이커 라이브러리로, 서비스 간의 호출 실패를 감지하고 시스템의 안정성을 유지합니다.
다양한 서킷 브레이커 기능을 제공하며, 장애 격리 및 빠른 실패를 통해 복원력을 높입니다.
💡
resilience4j 의존성은 spring starter에서 추가하여 사용하지 않을 것입니다.
실제 구현체가 포함된 GitHub의io.github.resilience4j:resilience4j-spring-boot3:2.2.0를 사용합니다.boot3임을 주의 하세요.
Spring Starter에서 제공하는 Resilience4j는 추상화 계층만 포함하고 있습니다.
Resilience4j를 사용하려면 Spring Boot 애플리케이션에 아래의 의존성을 추가해야 합니다.
dependencies {
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
Resilience4j의 설정은 application.yml 파일에서 설정할 수 있습니다.
실제 서비스에서는 Circuit Breaker 설정값을 현업(회사의 운영 환경)에 맞게 결정하며, 고정된 정답은 없고, 모니터링과 테스트를 거쳐 최적화해야 합니다
resilience4j:
circuitbreaker:
configs:
default: # 기본 구성 이름
registerHealthIndicator: true # 애플리케이션의 헬스 체크에 서킷 브레이커 상태를 추가하여 모니터링 가능
# 서킷 브레이커가 동작할 때 사용할 슬라이딩 윈도우의 타입을 설정
# COUNT_BASED: 마지막 N번의 호출 결과를 기반으로 상태를 결정
# TIME_BASED: 마지막 N초 동안의 호출 결과를 기반으로 상태를 결정
slidingWindowType: COUNT_BASED # 슬라이딩 윈도우의 타입을 호출 수 기반(COUNT_BASED)으로 설정
# 슬라이딩 윈도우의 크기를 설정
# COUNT_BASED일 경우: 최근 N번의 호출을 저장
# TIME_BASED일 경우: 최근 N초 동안의 호출을 저장
slidingWindowSize: 5 # 슬라이딩 윈도우의 크기를 5번의 호출로 설정
minimumNumberOfCalls: 5 # 서킷 브레이커가 동작하기 위해 필요한 최소한의 호출 수를 5로 설정
slowCallRateThreshold: 100 # 느린 호출의 비율이 이 임계값(100%)을 초과하면 서킷 브레이커가 동작
slowCallDurationThreshold: 60000 # 느린 호출의 기준 시간(밀리초)으로, 60초 이상 걸리면 느린 호출로 간주
failureRateThreshold: 50 # 실패율이 이 임계값(50%)을 초과하면 서킷 브레이커가 동작
permittedNumberOfCallsInHalfOpenState: 3 # 서킷 브레이커가 Half-open 상태에서 허용하는 최대 호출 수를 3으로 설정
# 서킷 브레이커가 Open 상태에서 Half-open 상태로 전환되기 전에 기다리는 시간
waitDurationInOpenState: 20s # Open 상태에서 Half-open 상태로 전환되기 전에 대기하는 시간을 20초로 설정
Fallback 메서드는 외부 서비스 호출이 실패했을 때 대체 로직을 제공하는 메서드입니다.
Fallback 메커니즘을 활용하면 외부 서비스 호출 실패 시 대체 로직을 제공해 시스템 안정성 및 장애 전파 차단 효과를 얻을 수 있습니다.
@Service
public class MyService {
@CircuitBreaker(name = "myService", fallbackMethod = "fallbackMethod")
public String myMethod() {
// 외부 서비스 호출
return externalService.call();
}
public String fallbackMethod(Throwable t) {
return "Fallback response";
}
}
Fallback 메서드 내에서는 장애 인지를 담당자가 빠르게 할 수 있게, 예를 들어 SMS나 슬랙 알림 등으로 관리자에게 통보하는 추가 로직 구현이 중요합니다. 최대한 빠르게 장애를 해결하는 것이 굉장히 중요하기 때문입니다.
MSA환경에서는 장애 발생 시 빠른 인지와 해결을 통해 서비스 신뢰도를 유지하는 것이 매우 중요합니다. Resilience4j 대시보드를 통해 Circuit Breaker의 상태를 실시간으로 모니터링할 수 있고, Prometheus및 Grafana연동으로 시각화가 가능합니다. 시각화는 관리자가 장애 상황을 빠르게 파악하고 대응하는 데 중요한 역할을 합니다.
dependencies {
implementation 'io.github.resilience4j:resilience4j-micrometer'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
}
management:
endpoints:
web:
exposure:
include: prometheus
prometheus:
metrics:
export:
enabled: true
http://${hostname}:${port}/actuator/prometheus에 접속하여 서킷브레이커 항목을 확인 가능합니다.
spring:
application:
name: my-service
cloud:
circuitbreaker:
resilience4j:
enabled: true
💡
이번 실습에서 유레카는 사용하지 않겠습니다.
프로젝트에서는 상품을 조회하는것을 가정하겠습니다.
상품 아이디 111을 호출하면 에러를 발생시켜 fallbackMethod 를 실행하는것을 확인합니다.
또한 이벤트리스너를 사용하여 서킷브레이커의 상태를 조회해보겠습니다.
start.spring.io 에 접속하여 프로젝트를 생성합니다. (디펜던시는 이미지 참고)
starter에서 resilience4j 디펜던시를 추가하지 않습니다. 직접 resilience4j를 사용하기 위해 build.gradle에 “io.github.resilience4j:resilience4j-spring-boot3:2.2.0”를 추가하겠습니다.

build.gradle의 dependencies에 다음을 추가합니다.
dependencies {
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
Resilience4j와 Spring Boot Starter AOP두 가지 의존성을 build.gradle 파일에 추가합니다. 자세한 내용은 공식 사이트를 확인하세요. resilience4j 가이드 문서
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
private String id;
private String title;
}
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable("id") String id) {
return productService.getProductDetails(id);
}
}
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ProductService {
private final Logger log = LoggerFactory.getLogger(getClass());
private final CircuitBreakerRegistry circuitBreakerRegistry;
@PostConstruct
public void registerEventListener() {
circuitBreakerRegistry.circuitBreaker("productService").getEventPublisher()
.onStateTransition(event -> log.info("#######CircuitBreaker State Transition: {}", event)) // 상태 전환 이벤트 리스너
.onFailureRateExceeded(event -> log.info("#######CircuitBreaker Failure Rate Exceeded: {}", event)) // 실패율 초과 이벤트 리스너
.onCallNotPermitted(event -> log.info("#######CircuitBreaker Call Not Permitted: {}", event)) // 호출 차단 이벤트 리스너
.onError(event -> log.info("#######CircuitBreaker Error: {}", event)); // 오류 발생 이벤트 리스너
}
@CircuitBreaker(name = "productService", fallbackMethod = "fallbackGetProductDetails")
public Product getProductDetails(String productId) {
log.info("###Fetching product details for productId: {}", productId);
if ("111".equals(productId)) {
log.warn("###Received empty body for productId: {}", productId);
throw new RuntimeException("Empty response body");
}
return new Product(
productId,
"Sample Product"
);
}
public Product fallbackGetProductDetails(String productId, Throwable t) {
log.error("####Fallback triggered for productId: {} due to: {}", productId, t.getMessage());
return new Product(
productId,
"Fallback Product"
);
}
// 이벤트 설명 표
// +---------------------------+-------------------------------------------------+--------------------------------------------+
// | 이벤트 | 설명 | 로그 출력 |
// +---------------------------+-------------------------------------------------+--------------------------------------------+
// | 상태 전환 (Closed -> Open) | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환되면 발생 | CircuitBreaker State Transition: ... |
// | 실패율 초과 | 설정된 실패율 임계치를 초과하면 발생 | CircuitBreaker Failure Rate Exceeded: ... |
// | 호출 차단 | 서킷 브레이커가 오픈 상태일 때 호출이 차단되면 발생 | CircuitBreaker Call Not Permitted: ... |
// | 오류 발생 | 서킷 브레이커 내부에서 호출이 실패하면 발생 | CircuitBreaker Error: ... |
// +---------------------------+-------------------------------------------------+--------------------------------------------+
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
// | 이벤트 | 설명 | 로그 출력 |
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
// | 메서드 호출 | 제품 정보를 얻기 위해 메서드를 호출 | ###Fetching product details for productId: ... |
// | (성공 시) 서킷 브레이커 내부에서 호출 성공 | 메서드 호출이 성공하여 정상적인 응답을 반환 | |
// | (실패 시) 서킷 브레이커 내부에서 호출 실패 | 메서드 호출이 실패하여 예외가 발생 | #######CircuitBreaker Error: ... |
// | (실패 시) 실패 횟수 증가 | 서킷 브레이커가 실패 횟수를 증가시킴 | |
// | (실패율 초과 시) 실패율 초과 | 설정된 실패율 임계치를 초과하면 발생 | #######CircuitBreaker Failure Rate Exceeded: ... |
// | (실패율 초과 시) 상태 전환 (Closed -> Open) | 연속된 실패로 인해 서킷 브레이커가 오픈 상태로 전환됨 | #######CircuitBreaker State Transition: Closed -> Open at ... |
// | (오픈 상태 시) 호출 차단 | 서킷 브레이커가 오픈 상태일 때 호출이 차단됨 | #######CircuitBreaker Call Not Permitted: ... |
// | (오픈 상태 시) 폴백 메서드 호출 | 메서드 호출이 차단될 경우 폴백 메서드 호출 | ####Fallback triggered for productId: ... due to: ... |
// +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
}
spring:
application:
name: sample
server:
port: 19090
resilience4j:
circuitbreaker:
configs:
default: # 기본 구성 이름
registerHealthIndicator: true # 애플리케이션의 헬스 체크에 서킷 브레이커 상태를 추가하여 모니터링 가능
# 서킷 브레이커가 동작할 때 사용할 슬라이딩 윈도우의 타입을 설정
# COUNT_BASED: 마지막 N번의 호출 결과를 기반으로 상태를 결정
# TIME_BASED: 마지막 N초 동안의 호출 결과를 기반으로 상태를 결정
slidingWindowType: COUNT_BASED # 슬라이딩 윈도우의 타입을 호출 수 기반(COUNT_BASED)으로 설정
# 슬라이딩 윈도우의 크기를 설정
# COUNT_BASED일 경우: 최근 N번의 호출을 저장
# TIME_BASED일 경우: 최근 N초 동안의 호출을 저장
slidingWindowSize: 5 # 슬라이딩 윈도우의 크기를 5번의 호출로 설정
minimumNumberOfCalls: 5 # 서킷 브레이커가 동작하기 위해 필요한 최소한의 호출 수를 5로 설정
slowCallRateThreshold: 100 # 느린 호출의 비율이 이 임계값(100%)을 초과하면 서킷 브레이커가 동작
slowCallDurationThreshold: 60000 # 느린 호출의 기준 시간(밀리초)으로, 60초 이상 걸리면 느린 호출로 간주
failureRateThreshold: 50 # 실패율이 이 임계값(50%)을 초과하면 서킷 브레이커가 동작
permittedNumberOfCallsInHalfOpenState: 3 # 서킷 브레이커가 Half-open 상태에서 허용하는 최대 호출 수를 3으로 설정
# 서킷 브레이커가 Open 상태에서 Half-open 상태로 전환되기 전에 기다리는 시간
waitDurationInOpenState: 20s # Open 상태에서 Half-open 상태로 전환되기 전에 대기하는 시간을 20초로 설정
management:
endpoints:
web:
exposure:
include: prometheus
prometheus:
metrics:
export:
enabled: true