[Spring Cloud] MSA : (9) 분산 시스템의 장애 격리 : CircuitBreaker & Resilience4j
마이크로서비스 간의 유기적인 연결이 많아질수록, 한 서비스의 장애로 인해 전체 시스템으로 확산되는 위험이 함께 존재한다.
분산 시스템의 궁극적인 목적은 Fault Tolerance(내결함성), 즉 시스템이 부분적인 장애나 실패 상황에서도 전체 기능을 계속 유지할 수 있도록 만드는 것이다.
바로 이때 필요한 것이 장애를 격리하고 시스템의 복원력을 지켜주는 장치로서 Circuit Breaker를 활용할 수 있다.
단순히 예외를 잡는 수준이 아니라, 실패가 반복되면 아예 호출 자체를 차단하고 우회(fallback)하게 함으로써 전체 장애로 번지는 것을 막아주는 역할을 한다.
이번 포스팅에서는 Circuit Breaker의 개념과 Spring Application에 적용해보는 과정을 다루어 보겠다.
CircuitBreaker
CircuitBreaker는 "회로 차단기"라는 뜻으로, 실제 차단기가 비정상적인 상태를 감지하면 다른 매개체로의 전파를 방지하는 것과 같은 역할을 한다.
- 문제가 발생한 서비스 대신 Fallback 로직으로 대체 수행 : 장애 회피 및 예외 상황에 대한 처리
- 장애가 발생한 서비스에 대해 호출 차단 : 예외 처리 수준을 넘어 의존 시스템이 비정상이라고 판단할 시, 호출 자체를 차단하여 전체 시스템의 지연 및 장애 확산 방지 & 리소스 절약
- Spring Cloud에서는 Resilience4j, Hystrix(구버전) 등의 라이브러리를 통해 구현 가능
- 모니터링 : 상태 전이를 적절히 관리하기 위해 메트릭 수집 및 대시보드를 연동하는 것이 권장됨 (Prometheus, Grafana 등)
사용 목적
- 장애 전파 방지 및 시스템 복원력(Fault Tolerance) 확보를 위한 핵심 메커니즘
- 마이크로서비스 아키텍처에서 서비스 간 호출 시, 장애가 전체로 확산되는 것을 막기 위해 활용
- Circuit Breaker는 API 호출, gRPC, FeignClient처럼 한 서비스가 다른 서비스의 응답에 직접 의존하는 동기 통신 구조에서, 장애 전파를 막기 위해 사용된다.
- 예를 들어 주문 시스템과 결제 시스템과 연동되었다고 가정해보자. 주문 시스템은 내부적으로 결제 서비스에 요청을 보낸다.
- 결제 서비스가 일시적으로 장애 상태일 때, 주문 요청을 차단하고 실패 처리한다.
- 이를 대신하여 Fallback으로 “결제 준비 중” 상태로 처리하고, 나중에 재시도하거나 수동 처리한다.
- 반면 Kafka나 SQS 같은 메시지 큐를 사용하는 비동기 통신 구조에서는 서비스 간 연결이 느슨하게 유지되므로, Circuit Breaker보다 Dead Letter Queue 사용, 보상 트랜잭션, SAGA Pattern와 같은 다른 복원 전략이 더 적절하게 사용된다.
처리 방식
- Metrics 수집: CircuitBreaker는 내부적으로 요청 수, 성공 수, 실패 수, 예외 종류 등의 메트릭을 집계
- Sliding Window: 최근 N초 또는 N개의 호출 기준으로 통계를 분석 (Resilience4j 기준 Sliding Window 기반)
- CircuitBreaker의 상태
- Closed 상태: 서비스가 정상이며, 외부 호출이 계속 시도되는 기본 상태
- Open 상태: 설정된 실패율 이상으로 오류가 발생하면 호출을 차단하고 즉시 fallback 실행
- Half-Open 상태: 일정 시간 후 일부 요청만 시도해 회복 여부를 판단, 성공 시 Closed로 복구
- 상태 전이:
- Failure Threshold: 설정된 실패율 이상이면 Open 상태로 전환 → 실패율(%)이 임계값 이상일 경우 Open 상태로 전환
- 대기 시간(Wait Duration in Open State): Open 상태 유지 시간 동안 호출 차단
- Half-Open(Open 상태 유지 시간 이후 Half-Open) → 성공률 확인 후 상태 복구
- Permitted Calls: Half-Open 상태에서는 일부 요청만 허용하여 회복 여부 판단
- 상태 처리
- Fallback 메커니즘: 실패 시 사전에 정의된 대체 응답 혹은 축소된 기능으로 서비스 지속
- Fail-fast 동작: Open 상태에서는 호출을 시도하지 않고 즉시 실패 처리 → 빠른 응답, 리소스 낭비 방지
CircuitBreaker 적용
다음 상황을 가정해보자.
- 회원(User) 데이터를 조회하기 위해 User Service에 요청한다.
- 이 때 회원이 주문한 내역을 함께 조회하기 위해 내부적으로 User Service는 Order Service와 통신하여 주문 내역을 응답받는다.
- Order Service의 응답을 회원(User) 데이터와 합쳐서 User Service는 최종 응답을 한다고 생각해보자.
만약 이때 Order Service가 장애로 인해 응답하지 않거나, 지연이 심한 상황이라면 어떻게 될까?
User Service는 Order Service의 응답을 기다리는 동안 함께 지연되거나 타임아웃이 발생하고, 결국 클라이언트는 회원 정보조차 받아보지 못하는 상황이 된다. 그리고 해당 지연시간으로 인해 곧 전체 시스템의 오버헤드와 지연 시간이 증가하게 될 것이다.
이러한 장애 전파를 막기 위해, User Service에는 Circuit Breaker를 적용할 수 있다.
일정 횟수 이상 Order Service 응답 실패가 감지되면 Circuit Breaker는 해당 호출을 차단하고 Order 정보 없이 회원 정보만 제공하는 등의 Fallback을 통해 전체 서비스의 안정성을 유지할 수 있다.
@FeignClient(name="order-service")
public interface OrderServiceClient {
@GetMapping("/order-service/{userId}/orders")
List<ResponseOrder> getOrders(@PathVariable String userId);
}
@Override
public UserDto getUserByUserId(String userId) {
UserEntity userEntity = userRepository.findByUserId(userId);
if(userEntity == null){
throw new UsernameNotFoundException("User Not Found");
}
UserDto dto = new ModelMapper().map(userEntity, UserDto.class);
List<ResponseOrder> orderList = null;
/** FeignClient를 이용하여 OrderService 호출 **/
log.info("Before call Order Service");
orderList = orderServiceClient.getOrders(userId);
log.info("After called Order Service");
dto.setOrders(orderList);
return dto;
}
- 위 코드를 보면 UserService는 회원 정보를 조회한 뒤 FeignClient를 통해 OrderService의 주문 내역을 동기적으로 호출한다.
- OrderService가 장애 상태이거나 응답이 느릴 경우 getOrders(userId) 호출이 타임아웃되거나 예외를 던지게 된다.
- UserService도 함께 그 응답을 기다리느라 지연되며 함께 실패하게 된다.
- 이런 상황이 반복되면 UserService의 스레드가 고갈되어 전체 서비스 장애로 이어질 수 있다.
- 위의 코드만으로는 OrderService가 동작하지 않을 때, UserService는 지연 시간과 함께 결국 어떤 데이터도 응답하지 못하고 Error만 반환하게 된다.
- 결국 OrderService의 장애 상태인지를 "판별"하고 "차단"시켜 줄 회로 역할, 그리고 장애 시 대체할 수 있는 행동을 Fallback 처리하기 위해 CircuitBreaker가 필요한 것이다.
의존성 설정 : Resilience4j
Circuit Breaker를 포함하여 Retry, Rate Limiter, Bulkhead, TimeLimiter 등의 장애 처리에 필요한 기능을 제공하는 Fault-Tolerance를 위한 Java 라이브러리이다.
▶ pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
CircuitBreaker 설정
@Configuration
public class Resilience4JConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> configuration(){
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(10)
.waitDurationInOpenState(Duration.ofMillis(1000))
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(2)
.build();
TimeLimiterConfig timeLimiterConfig = TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(10))
.build();
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(timeLimiterConfig)
.circuitBreakerConfig(circuitBreakerConfig)
.build());
}
}
Circuit Breaker 인스턴스를 생성하는 Circuit Breaker Factory에 공통으로 적용할 기본 설정을 Configuration 클래스에 Factory Bean을 정의해주었다. Circuit Breaker 사용자 설정과 관련된 값들을 조절할 수 있다.
- failureRateThreshold(10) : 실패율 임계값 (% 기준) → 최근 호출 10% 이상이 실패하면 Circuit Breaker가 Open 상태로 전환
- waitDurationInOpenState(Duration.ofMillis(1000)) : Open 상태 유지 시간 → 1초 동안 모든 호출을 차단한 후 Half-Open 상태로 전환
- slidingWindowType(COUNT_BASED) : 슬라이딩 윈도우 유형
- COUNT_BASED: 호출 횟수 기준
- TIME_BASED: 시간 기준
- slidingWindowSize(2) : 슬라이딩 윈도우 크기 → 최근 2번의 호출 결과로 실패율 계산 (COUNT_BASED)
CircuitBreaker 적용
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final OrderServiceClient orderServiceClient;
private final CircuitBreakerFactory circuitBreakerFactory;
@Override
public UserDto getUserByUserId(String userId) {
UserEntity userEntity = userRepository.findByUserId(userId);
if(userEntity == null){
throw new UsernameNotFoundException("User Not Found");
}
UserDto dto = new ModelMapper().map(userEntity, UserDto.class);
List<ResponseOrder> orderList = null;
log.info("Before call Order Service");
// CircuitBreaker 적용
CircuitBreaker cirbuitbreaker = this.circuitBreakerFactory.create("order-service");
orderList = cirbuitbreaker.run(
() -> orderServiceClient.getOrders(userId), //성공
throwable -> new ArrayList<>() //실패
);
log.info("After called Order Service");
dto.setOrders(orderList);
return dto;
}
}
- CircuitBreakerFactory를 사용해 "order-service"라는 ID의 Circuit Breaker 인스턴스 생성 (Config에서 작성한 정책 적용)
- 정상 흐름 : Circuit Breaker가 Closed 상태라면 실제 외부 서비스 호출 시도 → 응답이 성공하면 그대로 반환하고, 실패율에 따라 상태를 유지
- Fallback 처리 → Circuit Breaker가 Open 상태이거나 외부 호출에서 예외가 발생하면 즉시 Fallback 로직 실행 → Empty List를 반환하여 주문 정보만 비워 처리
Circuit Breaker의 ID
- CircuitBreaker의 ID는 설정과 상태를 구분하는 Key로 사용된다.
- 동일 ID를 사용하면 여러 호출 지점이 같은 상태(Circuit 상태, 실패율 등)를 공유한다. 독립적인 제어가 필요할 경우 ID를 분리해야 한다.
- 예를 들어 A 호출과 B 호출 시 동일한 ID를 사용하는 경우 : A 호출로 인해 Open 상태로 변경되면 B 호출 또한 Fallback처리된다.
결과 확인
- 위 결과는 Circuit Breaker가 적용된 실제 호출 흐름의 차이를 보여준다.
- 첫 번째는 OrderService가 정상 응답한 경우로, 회원 정보와 함께 주문 내역이 포함되어 응답된다.
- 반면 두 번째는 OrderService가 장애 상태일 때 Circuit Breaker가 호출을 차단하고 Fallback 처리하여 회원 정보만 반환되고 주문 목록은 비어 있는 상태로 처리되었다.
- 결국 서비스를 유지하면서 부분 실패에 대한 장애 확산이 Circuit Breaker에 의해 격리된 것을 확인할 수 있다.
References
Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) 강의 | Dowon Lee - 인프런
Dowon Lee | , 클라우드 네이티브 아키텍처를 구축하고, 마이크로서비스 앱 개발에 도전하세요! 🚧 [사진] IT 시스템에는 매년 수많은 기술이 생겨나고, 사라지고 있습니다. 새롭게 출시된 개념이나
www.inflearn.com
https://resilience4j.readme.io/docs/circuitbreaker
CircuitBreaker
Getting started with resilience4j-circuitbreaker
resilience4j.readme.io
'Backend > Spring Cloud MSA' 카테고리의 다른 글
[Spring Cloud] MSA : (10) 분산 추적 : Zipkin & Spring Cloud Sleuth (2) | 2025.05.06 |
---|---|
[Spring Cloud] MSA : (8) Apache Kafka 연결 : 데이터베이스 동기화 (1) | 2024.01.07 |
[Spring Cloud] MSA : (7) Apache Kafka 연결 : 서비스 간 비동기 통신 (1) | 2024.01.06 |
[Spring Cloud] MSA : (6) 서비스 간 동기적 통신 : RestTemplate과 FeignClient (1) | 2023.12.04 |
[Spring Cloud] MSA: (5) RabbitMQ와 Spring Cloud Bus로 Config 구성정보 동기화하기 (1) | 2023.11.26 |