서킷 브레이커

이진일·2026년 4월 15일
post-thumbnail

서킷 브레이커란?

  • 서킷 브레이커는 마이크로서비스 간의 호출 실패를 감지하고 시스템의 전체적인 안정성을 유지하는 패턴
  • 외부 서비스 호출 실패 시 빠른 실패를 통해 장애를 격리하고, 시스템의 다른 부분에 영향을 주지 않도록 한다.
  • 상태 변화: 클로즈드 -> 오픈 -> 하프-오픈

Resilience4j란?

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

Resilience4j의 주요 특징

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

서킷 브레이커 상태

클로즈드(Closed)

  • 기본 상태로 모든 요청을 통과시킨다.
  • 이 상태에서 호출이 실패하면 실패 카운터가 증가한다.
  • 실패율이 설정된 임계값을 초과하면 서킷 브레이커가 오픈 상태로 전환된다.

오픈(Open)

  • 서킷 브레이커가 오픈 상태로 전환되면 모든 요청을 즉시 실패로 처리한다.
  • 이 상태에서 요청이 실패하지 않고 바로 에러 응답을 반환한다.
  • 설정된 대기 시간이 지난 후 서킷 브레이커는 하프-오픈 상태로 전환된다.

하프-오픈(Half-Open)

  • 오픈 상태에서 대기 시간이 지나면 서킷 브레이커는 하프-오픈 상태로 전환된다.
  • 하프-오픈 상태에서는 제한된 수의 요청을 허용하여 시스템이 정상 상태로 복구되었는지 확인한다.
  • 요청이 성공하면 서킷 브레이커는 클로즈드 상태로 전환된다.

Resilience4j 설정

Resilience4j 의존성 추가

build.gradle

dependencies {
	implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

io.github.resilience4j:resilience4j-spring-boot3:2.2.0를 사용할 것이다. Spring Starter에서도 Resilience4j를 제공하는데 io.github.resilience4j:resilience4j-spring-boot3:2.2.0를 사용하는 이유는 Spring Starter에서 제공하는 Resilience4j는 추상화 계층을 포함하고 실제 구현체는 포함되어 있지 않다. 그렇기 때문에 이미 구현체가 있는 io.github.resilience4j:resilience4j-spring-boot3:2.2.0를 사용할 것이다.

물론 Spring Starter에서 제공하는 Resilience4j를 사용하여 직접 구현체를 구현하여 사용할 수도 있다.

Resilience4j 설정 파일

application.yml


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

application.yml파일 통해 다양한 설정을 할 수 있다.

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";
    }
}
  • myMethod()에서 외부 서비스 호출 시 실패했을 때 @CircuitBreakerfallbackMethod로 지정된 fallbackMethod()가 대체 로직으로 실행된다.

Fallback의 장점

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

서킷 브레이커 실습

Product.java

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {

    private String id;
    private String title;

}

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);
    }
}

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: ...           |
    // +------------------------------------------+-------------------------------------------+-----------------------------------------------------------------+

}

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
  • /product/11 엔드포인트로 호출하면 getProductDetails()가 정상 실행되어 "Sample Product"를 반환한다.
  • /product/111 엔드포인트로 호출하면 RuntimeException이 발생하고, @CircuitBreaker가 예외를 감지하고 fallbackGetProductDeatils를 실행하여 "Fallback Product"를 반환한다. (클라이언트는 에러 대신 fallback 응답을 받는다.)
  • /product/111 로 3번이상 반복하여 호출하면 설정한 임계값(failureRateThreshold) 50%를 초과하여 오픈 상태로 전환된다.
  • 오픈 상태에서 id가 아닌 값으로 요청을 보내도 폴백 메서드가 실행되어 "Fallback Product"를 반환한다.
  • 이때는 getProductDetails()를 거치지 않고 바로 폴백 메서드가 실행되므로 "###Fetching product details for productId: {}"로그가 찍히지 않는다.
  • 20초(waitDurationInOpenState)가 지난후 서킷 상태가 하프-오픈 상태로 전환된다.
  • 하프-오픈 상태에서 3번(permittedNumberOfCallsInHalfOpenState)의 요청이 모두 성공하면 다시 클로즈드 상태로 돌아가고 1번이라도 실패하면 다시 오픈 상태로 전환된다.

0개의 댓글