서킷브레이커

미미·2025년 9월 24일

MSA

목록 보기
4/5

서킷브레이커란?

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

  • 상태 변화 : 클로즈드 -> 오픈 -> 하프-오픈

Resilience4j

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

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

서킷 브레이커 상태

: 클로즈드, 오픈, 하프-오픈 상태를 통해 호출 실패를 관리합니다.

  • 클로즈드(Closed):
    • 기본 상태로, 모든 요청을 통과시킵니다.
    • 이 상태에서 호출이 실패하면 실패 카운터가 증가합니다.
    • 실패율이 설정된 임계값(예: 50%)을 초과하면 서킷 브레이커가 오픈 상태로 전환됩니다.
    • EX) 최근 5번의 호출 중 3번이 실패하여 실패율이 60%에 도달하면 오픈 상태로 전환됩니다.
  • 오픈(Open):
    • 서킷 브레이커가 오픈 상태로 전환되면 모든 요청을 즉시 실패로 처리합니다.
    • 이 상태에서 요청이 실패하지 않고 바로 에러 응답을 반환합니다.
    • 설정된 대기 시간이 지난 후, 서킷 브레이커는 하프-오픈 상태로 전환됩니다.
    • EX) 서킷 브레이커가 오픈 상태로 전환되고 20초 동안 모든 요청이 차단됩니다.
  • 하프-오픈(Half-Open):
    • 오픈 상태에서 대기 시간이 지나면 서킷 브레이커는 하프-오픈 상태로 전환됩니다.
    • 하프-오픈 상태에서는 제한된 수의 요청을 허용하여 시스템이 정상 상태로 복구되었는지 확인합니다.
    • 요청이 성공하면 서킷 브레이커는 클로즈드 상태로 전환됩니다.
    • 요청이 다시 실패하면 서킷 브레이커는 다시 오픈 상태로 전환됩니다.
    • EX) 하프-오픈 상태에서 3개의 요청을 허용하고, 모두 성공하면 클로즈드 상태로 전환됩니다. 만약 하나라도 실패하면 다시 오픈 상태로 전환됩니다.

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

@CircuitBreakerfallbackMethod로 에러 발생시 호출할 함수이름을 넣어주면 에러 발생시 fallbackMethod가 호출됩니다.

Dashboard

Resilience4j Dashboard를 사용하여 서킷 브레이커의 상태를 모니터링할 수 있습니다.

  • Prometheus와 Grafana를 사용하여 Resilience4j 서킷 브레이커의 상태를 실시간으로 모니터링할 수 있습니다.
  • Prometheus를 통해 수집된 메트릭을 Grafana 대시보드에서 시각화할 수 있습니다.
  • http://${hostname}:${port}/actuator/prometheus 에 접속하여 서킷브레이커 항목을 확인 가능합니다.

Spring Cloud와의 통합

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

실습

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

1. 프로젝트 생성

resilience4j는 여기서 dependencies를 추가하지않고 따로 gradle에 추가해주겠습니다.
여기서 추가하는 dependencies는 추상화되어있는 상태에서 github에서 가져오겠습니다.
프로젝트를 생성하고 build.gradle에 아래의 의존성을 추가로 넣어줍니다.

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

2. 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
  • 서킷브레이커의 기본설정들을 추가해줍니다.
  • 대시보드를 보기위해 prometheus도 함께 설정해줍니다.

3. Product

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {

	private String id;
	private String title;

}

4. Product Service

@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"
		);
	}
  • @CircuitBreaker 어노테이션 name = "productService" → resilience4j 설정 파일(application.yml 등)에 정의된 productService CircuitBreaker를 사용합니다.
  • fallbackMethod = "fallbackGetProductDetails" → 원래 메서드가 예외를 던지거나 CircuitBreaker가 OPEN 상태라서 호출이 차단될 경우 실행됩니다.
  • 이벤트리스터 등록 @PostConstruct + CircuitBreakerRegistry → resilience4j의 CircuitBreaker에서 발생하는 이벤트를 감지하고 로그를 찍습니다.
    onStateTransition → 회로 상태(CLOSED, OPEN, HALF_OPEN) 변경 시 이벤트
    onFailureRateExceeded → 실패율이 설정한 기준치 이상일 때 이벤트
    onCallNotPermitted → 회로가 열려(Open) 있어서 호출 자체가 차단될 때 이벤트
    onError → 호출 중 에러가 발생했을 때 이벤트

5. ProductController

@RestController
@RequiredArgsConstructor
public class ProductController {
	private final  ProductService productService;

	@GetMapping("/product/{id}")
	public Product getProduct(@PathVariable String id){
		return productService.getProductDetails(id);
	}
}

실행

호출 시 정상적인 모습을 확인할 수 있습니다.
이제 111을 호출하여 에러를 발생시켜보겠습니다.


fallbackMethod가 작동되어 Fallback Product를 대신 리턴해주는것을 확인할 수 있습니다.

이제 다시 정상적으로 호출해보겠습니다.

에러 발생 후 서킷브레이커가 Open 상태가 되면서 getProductDetails 함수를 타지않고 바로 fallback Method가 호출 되는것을 확인 할 수 있습니다.

몇 번 더 정상적인 호출을 해보겠습니다.

 ####Fallback triggered for productId: 1 due to: CircuitBreaker 'productService' is OPEN and does not permit further calls
2025-09-24T10:25:15.236+09:00  INFO 39637 --- [sample] [io-19090-exec-8] c.s.r.sample.products.ProductService     : #######CircuitBreaker State Transition: 2025-09-24T10:25:15.236871+09:00[Asia/Seoul]: CircuitBreaker 'productService' changed state from OPEN to HALF_OPEN
2025-09-24T10:25:15.237+09:00  INFO 39637 --- [sample] [io-19090-exec-8] c.s.r.sample.products.ProductService     : ###Fetching product details for productId: 2
2025-09-24T10:26:15.670+09:00  INFO 39637 --- [sample] [io-19090-exec-1] c.s.r.sample.products.ProductService     : ###Fetching product details for productId: 2
2025-09-24T10:26:19.491+09:00  INFO 39637 --- [sample] [io-19090-exec-2] c.s.r.sample.products.ProductService     : ###Fetching product details for productId: 2
2025-09-24T10:26:19.492+09:00  INFO 39637 --- [sample] [io-19090-exec-2] c.s.r.sample.products.ProductService     : #######CircuitBreaker State Transition: 2025-09-24T10:26:19.492497+09:00[Asia/Seoul]: CircuitBreaker 'productService' changed state from HALF_OPEN to CLOSED
2025-09-24T10:26:19.958+09:00  INFO 39637 --- [sample] [io-19090-exec-3] c.s.r.sample.products.ProductService     : ###Fetching product details for productId: 2
2025-09-24T10:26:20.971+09:00  INFO 39637 --- [sample] [io-19090-exec-4] c.s.r.sample.products.ProductService     : ###Fetching product details for productId: 2

로그를 살펴보면 처음 OPEN 상태때문에 콜을 허용하지않다가 OPEN에서 HALF_OPEN상태로 바뀌고 제한된 수의 요청을 허용하고, 모두 성공하여 CLOSED 상태로 변경된것을 확인 할 수 있습니다.


시간이 지나 상태가 다시 closed되어 정상적으로 호출되는것을 볼 수 있습니다.

0개의 댓글