Resilience4j를 이용한 서킷 브레이커 패턴 구현

호기성세균·2024년 9월 12일
0

Project

목록 보기
16/16

서킷 브레이커란?
https://velog.io/@ghrltjdtprbs/MSA%EC%97%90%EC%84%9C-Circuit-Breaker-Pattern-%EC%9D%84-%EC%99%9C-%EC%82%AC%EC%9A%A9%ED%95%A0%EA%B9%8C

💡 Resilience4j란?

Resilience4j는 MSA에서 서비스의 신뢰성을 보장하기 위한 라이브러리다.
이 라이브러리는 서킷 브레이커, Rate Limiter, Retry, Bulkhead 등 다양한 패턴을 제공하여 외부 서비스나 자원에 의존하는 서비스들이 장애 상황에서도 안정적으로 작동할 수 있도록 돕는다.


💡 Resilience4j 채택 이유

  1. 경량성과 유연한 설정 제공
  2. 스프링 부트와의 뛰어난 호환성을 제공하며, 필요에 따라 다양한 내결함성 패턴을 쉽게 추가할 수 있음
  3. 구성과 확장이 용이하고, 추가적인 라이브러리 의존성을 최소화하기 때문에 MSA 환경에서의 효율성이 뛰어남
  4. Spring Cloud와 Netflix Eureka와의 호환성이 뛰어나, Spring 기반 프로젝트에서 서킷 브레이커 패턴을 간편하게 적용할 수 있다.
  5. 서킷 브레이커뿐만 아니라 Retry와 Rate Limiting, Bulkhead와 같은 다양한 내결함성 기능을 지원하기 때문에, 장애 상황에서도 서비스를 유연하게 관리할 수 있다.

이러한 이유로, 안정성은 물론 확장성과 경량성을 챙길 수 있는 Resilience4j를 선택하게 되었다.

📌 Resilience4j의 다양한 기능
1. Retry
실패한 요청을 일정 횟수만큼 재시도하는 패턴이다. 단순히 재시도하는 것뿐만 아니라, 재시도 간격을 설정하거나 점진적 대기 시간(Exponential Backoff) 을 적용하여 재시도 간 요청 부하를 줄일 수 있다. 외부 서비스나 자원에 일시적인 문제가 있을 때 바로 실패로 처리하지 않고, 정해진 횟수만큼 재시도하여 장애를 최소화할 수 있다.
2. Rate Limiting
특정 시간 동안 시스템이 처리할 수 있는 요청의 수를 제한하는 패턴이다. 시스템이 과부하에 빠지지 않도록 초당 혹은 분당 처리할 수 있는 최대 요청 수를 설정한다. 초과된 요청은 대기시키거나 거부할 수 있으며, 이는 주로 DDoS 공격이나 과도한 트래픽으로 인해 서비스가 불안정해지는 상황을 방지하기 위해 사용된다.
3. Bulkhead
시스템의 자원을 격리하여 한 부분에서 발생한 장애가 다른 부분에 영향을 미치지 않도록 하는 패턴이다. 예를 들어, 특정 서비스에 할당된 쓰레드 풀이나 연결 수를 제한함으로써 다른 서비스나 기능이 영향을 받지 않도록 보호할 수 있다. 이를 통해 서비스 간 격리성을 강화하여 시스템의 안정성을 높일 수 있으며, 하나의 서비스에서 장애가 발생해도 나머지 서비스는 정상적으로 작동할 수 있도록 보장한다.


💡 프로젝트 적용

1. 의존성 추가

build.gradle 파일에 Resilience4j 관련 의존성을 추가한다.

    implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'

2. 서킷 브레이커 설정

application.yml 파일에 서킷 브레이커의 동작 방식을 정의한다.

resilience4j:
  circuitbreaker:
    instances:
      productServiceClient:
        slidingWindowSize: 10
        failureRateThreshold: 40
        waitDurationInOpenState: 10000
        permittedNumberOfCallsInHalfOpenState: 3
        minimumNumberOfCalls: 10
        eventConsumerBufferSize: 10
  • slidingWindowSize: 장애 감지를 위한 요청 수의 윈도우 크기
  • failureRateThreshold: 요청 실패율이 해당 값을 넘을 때 서킷을 열어 장애 상태로 전환
  • waitDurationInOpenState: 서킷이 열려 있는 상태에서 대기하는 시간
  • permittedNumberOfCallsInHalfOpenState: 서킷이 Half Open 상태로 전환되었을 때 허용되는 요청 수
  • minimumNumberOfCalls: 서킷 브레이커가 동작하기 위해 필요한 최소 요청 수

3. 서킷 브레이커 어노테이션 적용

Feign 클라이언트에 서킷 브레이커를 적용하려면 @CircuitBreaker 어노테이션을 사용하면 된다. 각 메서드에 적용하며, 장애가 발생했을 경우 대체할 Fallback 메서드를 지정할 수 있다.

@FeignClient(name = "product-service")
public interface ProductServiceClient {

    Logger logger = LoggerFactory.getLogger(ProductServiceClient.class);

    @CircuitBreaker(name = "productServiceClient", fallbackMethod = "checkOptionExistsFallback")
    @GetMapping("/product-service/options/{optionId}/exists")
    ResponseEntity<Boolean> checkOptionExists(@PathVariable("optionId") Long optionId);

    @CircuitBreaker(name = "productServiceClient", fallbackMethod = "getOptionStockFallback")
    @GetMapping("/product-service/options/{optionId}/inventory")
    ResponseEntity<Integer> getOptionStock(@PathVariable("optionId") Long optionId);

    @CircuitBreaker(name = "productServiceClient", fallbackMethod = "getOptionDetailsFallback")
    @GetMapping("/product-service/options/{optionId}/details")
    ResponseEntity<OptionDetailDTO> getOptionDetails(
        @PathVariable("optionId") Long optionId);

    @CircuitBreaker(name = "productServiceClient", fallbackMethod = "getMaxPurchaseLimitFallback")
    @GetMapping("/product-service/options/{optionId}/max-purchase-limit")
    ResponseEntity<Integer> getMaxPurchaseLimit(
        @PathVariable("optionId") Long optionId);

    @CircuitBreaker(name = "productServiceClient", fallbackMethod = "updateOptionStockFallback")
    @PutMapping("/product-service/options/{optionId}/inventory")
    ResponseEntity<ResponseDTO<Void>> updateOptionStock(@PathVariable("optionId") Long optionId,
        @RequestBody int stock);

    // Fallback Methods
    default ResponseEntity<Boolean> checkOptionExistsFallback(Long optionId,
        Throwable throwable) {
        logError(throwable);
        return ResponseEntity.ok(false);
    }

    default ResponseEntity<Integer> getOptionStockFallback(Long optionId,
        Throwable throwable) {
        logError(throwable);
        return ResponseEntity.ok(0);
    }

    default ResponseEntity<OptionDetailDTO> getOptionDetailsFallback(Long optionId,
        Throwable throwable) {
        logError(throwable);
        OptionDetailDTO fallbackOption = OptionDetailDTO.builder()
            .optionId(optionId)
            .optionType("N/A")
            .availability(false)
            .stock(0)
            .productId(-1L)
            .productName("Unknown Product")
            .build();
        return ResponseEntity.ok(fallbackOption);
    }

    default ResponseEntity<Integer> getMaxPurchaseLimitFallback(Long optionId,
        Throwable throwable) {
        logError(throwable);
        return ResponseEntity.ok(1);
    }

    default ResponseEntity<Void> updateOptionStockFallback(Long optionId, int stock,
        Throwable throwable) {
        logError(throwable);
        return ResponseEntity.ok().build();
    }

    private void logError(Throwable throwable) {
        if (throwable instanceof FeignException) {
            logger.error("product-service에 문제 발생! fallback method 호출 : {}", throwable.getMessage());
        } else {
            logger.error("product-service 에러! {}", throwable.getMessage());
        }
    }
}

위 코드에서 @CircuitBreaker 어노테이션을 통해 productServiceClient 서킷 브레이커가 동작하도록 설정했다. 만약 장애가 발생하면 지정된 Fallback 메서드가 호출되며, 이를 통해 서비스의 안정성을 유지할 수 있다.

4. Fallback 메서드 구현

Fallback 메서드는 장애 상황에서 호출되는 대체 로직을 처리한다.
위 코드에서는, checkOptionExistsFallback 메서드를 통해 서비스가 정상적으로 작동하지 않더라도 기본적으로 false 값을 반환하도록 했다.
또한 Fallback 메서드 호출시 로그로 "product-service 에러!"를 남기도록 구현하였다.

📌 Fallback 메서드
단순히 장애 발생 시 예외를 처리하는 역할을 넘어, 서비스 복구 전까지 대체 경로를 제공하거나 캐시된 데이터를 반환하는 방식으로 활용할 수 있다.
이를 통해 사용자에게는 장애가 발생하지 않은 것처럼 보이게 할 수 있으며, 장애가 복구될 때까지 서비스의 연속성을 보장할 수 있다.
또한, Fallback 메서드 내부에서 장애 상황을 로그로 남겨, 시스템 복구 이후 장애 원인을 분석하는 데 도움을 줄 수 있다.


💡 테스트

order-service와 통신하는 product-service를 내려 장애 상황을 연출하였다.

❗️ 서킷브레이커 적용 전

product-service에 문제가 생기자 503 에러가 난다.


❗️ 서킷브레이커 적용 후

우선 Spring Actuator 를 통해 서킷브레이커의 상태를 먼저 체크해보자.
어떠한 요청도 보내지 않았으니 서킷브레이커는 CLOSED 상태이다.

💡 예상 동작

  1. 설정대로 10개의 요청 중 40%(4개) 의 요청이 실패하면 서킷 브레이커가 OPEN 상태로 전환되어야 한다.
  2. 10초 후 서킷브레이커는 HALF_OPEN상태로 전환되어야 한다.
  3. 모든 실패 요청은 Fallback 메서드로 인해 마치 문제가 없는 것처럼 동작하여야한다.

그럼 이제 실제로 예상대로 동작하는지 확인해보도록 하자

Fallback 메서드 호출 확인

503에러를 내보내지 않고 잘 동작한다.
(해당 404에러는 order-service에 따로 예외처리를 해둔 것으로 정상적인 동작이다.)

실패하는 요청을 10번 보낸 후 서킷브레이커 확인

서킷브레이커가 OPEN 상태로 전환되어 있는 것을 확인할 수 있다.

10초 후 서킷브레이커 상태 확인

예상대로 HALF_OPEN 상태로 전환되었다.

성공 요청을 보낸 후 서킷브레이커 상태 확인

이제 product-server를 다시 띄운 후, 요청 성공시 서킷브레이커 상태가 다시 CLOSED로 변환되는지 확인해보자.

이로써 product-service 장애 발생 시, 클라이언트는 Fallback 메서드를 통해 정상적인 응답을 받을 수 있으며, 서킷 브레이커를 통해 장애가 다른 서비스로 전파되지 않도록 요청을 차단하는 기능이 완성되었다.

profile
공부...열심히...

0개의 댓글