본문 바로가기

Study/Spring

[Spring] Spring-Cloud를 활용한 MSA 설계(2) - 서킷 브레이커

 

[Spring] Spring-Cloud를 활용한 MSA 설계(1) - 서비스 디스커버리, 로드 밸런싱

[Spring] Spring-Cloud를 활용한 MSA 설계(0) - IntroSpring-Cloud를 통해 MSA 설계를 하기에 앞서,MSA의 개념과 Spring-Cloud의 주요 모듈 및 구성 요소를 정리한 서론입니다.   ✅ MSA란?MSA(Micro Services Architecture)

zapzook.tistory.com

 

지난 포스팅에서 다뤘던 서비스 디스커버리, 로드 밸런싱에 이어서

이번 포스팅에선 서킷 브레이커에 대해 자세히 알아보고, 실습을 진행할 예정이다.

서킷 브레이커 (Resilience4j)

 

서킷 브레이커는 마이크로 서비스간의 호출 실패를 감지하고 시스템의 전체적인 안정성을 유지하는 패턴이다.

외부 서비스 호출 실패 시 빠른 실패를 통해 장애를 격리하고, 시스템의 다른 부분에 영향을 주지 않도록 한다.

 

예를 들어서 주문 서비스와 결제 서비스로 나뉜 마이크로 서비스가 있다고 하자.
주문 서비스가 결제 서비스를 호출할 때 결제 서비스가 다운되거나 느린 경우, 서킷 브레이커가 이를 감지하고 더 이상의 호출을 차단한다. 이로 인해 주문 서비스가 무한 대기하거나 장애 전파로 인해 다운되는 것을 방지할 수 있다.

📌 Reslilence4j

 

Resilience4j는 서킷 브레이커 라이브러리로, 서비스 간의 호출 실패를 감지하고 시스템의 안정성을 유지한다.

다양한 서킷 브레이커 기능을 제공하며, 장애 격리 및 빠른 실패를 통해 복원력을 높인다.

주요 특징

  • 서킷 브레이커 상태 : closed, open, half-open << 세 가지 상태를 통해 호출 실패를 관리한다
  • Fallback: 호출 실패 시 대체 로직을 제공하여 시스템 안정성을 확보한다
  • 모니터링 : 서킷 브레이커 상태를 모니터링하고 관리할 수 있는 다양한 도구를 제공한다
Fallback? Failback?

 

서킷 브레이커를 공부할 때, 어디서는 Fallback이라고 말하고 어디서는 Failback이라고 말한다...

Fallback : 서비스 호출 실패 시, 대체 방법을 제공 (예: "결제 서비스가 불가능합니다.")

Failback : 서비스가 복구되면, 원래 경로로 돌아가는 것

즉, 서킷 브레이커에서 사용하는 정확한 용어는 Fallback이 맞다!

 

📌 Fallback 매커니즘

Fallback 메서드는 외부 서비스 호출이 실패했을 때 대체 로직을 제공하는 메서드이다.

장점으로는 시스템의 안정성을 높이고, 장애가 발생해도 사용자에게 일정한 응답을 제공할 수 있다.

또, 장애가 다른 서비스에 전파되는 것을 방지할 수 있다.

강사분이 설명하시길 에러를 에러가 아닌 것처럼 고객에게 보여줄 수 있다는 부분에서 이점이 크다고 한다..

 

📌 Resilience4j 대시보드 설정

 

build.gradle


dependencies {
    implementation 'io.github.resilience4j:resilience4j-micrometer'
    implementation 'io.micrometer:micrometer-registry-prometheus'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
}

application.yml

management:
  endpoints:
    web:
      exposure:
        include: prometheus
  prometheus:
    metrics:
      export:
        enabled: true

 

위와 같이 설정을 해주면 서킷 브레이커의 상태를 모니터링 할 수 있다.

localhost:{port}/actuator/prometheus 에 접속하면 Prometheus를 통해 수집된 메트릭을 확인할 수 있다.

이후 수집된 메트릭을 Grafana 대시보드를 통해 시각화 할 수 있는데, 이 부분은 본 포스팅에선 다루지 않을 예정이다.

그냥 이런게 있고 가능하다는 것만 알아두자

서킷 브레이커 (Resilience4j) 실습

이번 실습에서는 Eureka는 사용하지 않고, 서킷 브레이커의 fallbackMethod를 확인하는데 중점을 두겠다.

또, 이벤트 리스터를 사용해 서킷브레이커의 상태를 조회해볼 예정이다.

 

📌 build.gradle

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

    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    runtimeOnly 'io.micrometer:micrometer-registry-prometheus'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test'){
        useJUnitPlatform()
}

 

resilience4j 사용을 위해 디펜던시를 추가해준다.

단, Spring starter에서 디펜던시를 추가하면 추상화 계층의 Resilience가 사용되기에

꼭 명시된 디펜던시를 추가해줘야 한다.

 

📌 Product.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {

    private String id;
    private String title;

}

📌 ProductController.java

@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

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



}

 

상품 번호 111이 입력되면 서킷 브레이커를 통해 fallback 메서드가 실행되도록 구성하였다.

 

📌 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

 

상품 번호 111을 호출하여 서킷 브레이커가 Open 상태가 되면 getProductDetails 함수를 타지 않고

바로 fallbackGetProductDetails 로 호출 되는 것을 확인할 수 있다.

 

 

이후 localhost:{port}/actuator/prometheus 로 접속하면 인스턴스의 상태값들을 확인할 수 있다.

이렇게 보면 가독성이 굉장히 떨어지는데, 이 때문에 Grafana라는 시각화 툴이 존재하는 것