[MSA] 서킷브레이커(Resilience4j)

박원준·2026년 3월 25일

MSA

목록 보기
5/9

1️⃣ 서킷 브레이커란?

서킷 브레이커는 마이크로서비스 간의 호출 실패를 감지해 전체 시스템 안정성을 유지하는 패턴입니다. 외부 서비스 호출 실패 시 빠른 실패를 통해 장애를 격리하고, 시스템의 다른 부분에 영향을 주지 않도록 합니다.

서킷 브레이커는 Closed, Open, Half-Open 세 가지 상태를 가지며, 각각의 상태에 따라 요청을 통과시키거나 차단합니다.


🔹Closed

먼저 Closed 상태는 모든 요청을 통과시키는 기본 상태이며, 실패율이 임계값(예: 50% 이상)을 넘으면 Open 상태로 전환됩니다. 예를 들어 최근 5번의 호출 중 3번이 실패하여 실패율이 60%에 도달하면 오픈 상태로 전환됩니다. 즉, Closed 상태에서는 성공과 실패의 횟수를 카운트합니다.


🔹 Open

Open 상태에서는 클라이언트가 요청을 보내면, 서킷 브레이커가 서버로 요청을 전달하지 않고 그 자리에서 즉시 거절합니다. 서버 응답을 기다리느라 5초, 10초씩 무한 로딩(타임아웃)이 걸리는 게 아니라, 0.1초 만에 바로 에러 응답을 보냅니다. 이때 단순히 에러만 던지는 게 아니라, 미리 준비한 "현재 서비스 점검 중입니다" 같은 안내 문구나 임시 데이터를 보여줄 수 있습니다. 또한 일정 대기 시간(예: 20초) 후 Half-Open 상태로 전환됩니다.
Open 상태에서는 왜 이렇게 할까요? 서버 보호와 장애 전파 방지를 해야하기 때문입니다.
이미 과부하로 비명 지르는 서버에 계속 요청을 보내면 서버가 완전히 타버릴 수 있습니다. 잠시 쉴 시간을 주는 거죠. 또한 한 서버가 느려지면 그 서버를 기다리는 다른 서비스들까지 줄줄이 대기 상태가 되어 시스템 전체가 마비됩니다. 이를 미리 끊어 전체 시스템을 살립니다.


🔹 Half-Open

Half-Open 상태에서는 제한된 요청만 허용해 시스템 복구 여부를 점검하며, 성공 시 Closed 로, 실패 시 Open으로 다시 전환합니다. 모든 요청을 다 보내는 게 아니라, 미리 정해둔 소수의 요청(예: 10개)만 실제 서버로 보내봅니다.
만약 요청이 성공하면 Closed(정상)로 복구합니다. 테스트 요청들이 모두 성공하면 "아, 서버가 이제 다 나았구나!"라고 판단해서 다시 길을 완전히 열어줍니다.
하나라도 실패하면 Open(차단)으로 회귀합니다. 테스트 중 에러가 발생하면 "아직 안 됐네"라고 판단하고, 즉시 다시 차단기(Open)를 내려버립니다. 그리고 다시 대기 시간을 갖습니다.
이는 관리자가 일일이 버튼을 눌러 서버를 재가동할 필요 없이, 시스템이 스스로 건강 상태를 체크하고 복구하기 위해서입니다. 또한 서버가 막 살아났는데 갑자기 수만 개의 요청이 쏟아지면 다시 죽을 수 있습니다. 그래서 조금씩 트래픽을 흘려보내며 서버를 보호하는 역할을 합니다.


2️⃣ Resilience4j

Resilience4j는 서킷 브레이커 라이브러리로, 서비스 간의 호출 실패를 감지하고 시스템의 안정성을 유지합니다.
다양한 서킷 브레이커 기능을 제공하며, 장애 격리 및 빠른 실패를 통해 복원력을 높입니다.

🔹 Resilience4j의 주요 특징

  • 서킷 브레이커 상태: 클로즈드, 오픈, 하프-오픈 상태를 통해 호출 실패를 관리합니다.
  • Fallback: 호출 실패 시 대체 로직을 제공하여 시스템 안정성 확보합니다.
  • 모니터링: 서킷 브레이커 상태를 모니터링하고 관리할 수 있는 다양한 도구 제공

3️⃣ 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초로 설정

4️⃣ Fallback 메커니즘

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나 슬랙 알림 등으로 관리자에게 통보하는 추가 로직 구현이 중요합니다. 최대한 빠르게 장애를 해결하는 것이 굉장히 중요하기 때문입니다.

🔹 Fallback의 장점

  • 시스템의 안정성을 높이고, 장애가 발생해도 사용자에게 일정한 응답을 제공할 수 있습니다.
  • 장애가 다른 서비스에 전파되는 것을 방지합니다.

5️⃣ Resilience4j Dashboard

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에 접속하여 서킷브레이커 항목을 확인 가능합니다.


6️⃣ Resilience4j와 Spring Cloud 연동

  • Resilience4j는 Spring Cloud Netflix 패키지의 일부로, Eureka와 Ribbon 등 다른 Spring Cloud 구성 요소와 쉽게 통합할 수 있습니다.
  • Spring Cloud의 서비스 디스커버리와 로드 밸런싱을 활용하여 더욱 안정적인 마이크로서비스 아키텍처를 구축할 수 있습니다.
spring:
  application:
    name: my-service
  cloud:
    circuitbreaker:
      resilience4j:
        enabled: true

7️⃣ 실습

💡
이번 실습에서 유레카는 사용하지 않겠습니다.
프로젝트에서는 상품을 조회하는것을 가정하겠습니다.
상품 아이디 111을 호출하면 에러를 발생시켜 fallbackMethod 를 실행하는것을 확인합니다.
또한 이벤트리스너를 사용하여 서킷브레이커의 상태를 조회해보겠습니다.

🔹 프로젝트 생성 및 코드 작성

  1. start.spring.io 에 접속하여 프로젝트를 생성합니다. (디펜던시는 이미지 참고)
    starter에서 resilience4j 디펜던시를 추가하지 않습니다. 직접 resilience4j를 사용하기 위해 build.gradle에 “io.github.resilience4j:resilience4j-spring-boot3:2.2.0”를 추가하겠습니다.

  2. 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 가이드 문서

  1. products 패키지와 Product, ProductController, ProductService 등 총 3개의 파일/클래스를 생성합니다.
java/com/spring_cloud/resilience4j/sample/products/Product.java
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {

    private String id;
    private String title;

}
java/com/spring_cloud/resilience4j/sample/products/ProductController.java
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);
    }
}
java/com/spring_cloud/resilience4j/sample/products/ProductService.java
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: ...           |
    // +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+
   
}
  1. resources/application.yml
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

🔹 Run

  • http://localhost:19090/products/11 를 3번 호출합니다.
  • http://localhost:19090/products/111 을 여러번 호출 하면서 서킷브레이커의 상태가 변경되는것을 확인합니다.
  • 또한 서킷브레이커가 Open 상태가 되면 getProductDetails 함수를 타지않고 바로 fallbackGetProductDetails 로 호출 되는것을 확인 할 수 있습니다.

🔹 프로메테우스 데이터 전달

  • http://localhost:19090/actuator/prometheus 로 접속하면 인스턴스의 상태 값들이 나옵니다.
  • 아래로 내려가다보면 서킷브레이커 관련 내용을 확인할 수 있습니다. 이를통해 프로메테우스에서 서킷브레이커의 정보를 수집할 수 있습니다. (프로메테우스 내용은 이후 모니터링 챕터에서 다루겠습니다.)

0개의 댓글