Hello, Freakin world!

Resilience4j로 간단한 서킷 브레이커 구현해보기 본문

Spring Cloud/Circuit breaker

Resilience4j로 간단한 서킷 브레이커 구현해보기

johnna_endure 2021. 3. 8. 17:58

스프링 이니셜라이저를 이용해 아래와 같은 의존성을 가지는 프로젝트를 생성합니다.

 

plugins {
    id 'org.springframework.boot' version '2.4.3'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'springboot.cloud'
version = '1.0.0'
sourceCompatibility = '11'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

ext {
    set('springCloudVersion', "2020.0.1")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
    implementation "io.github.resilience4j:resilience4j-spring-boot2"
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

test {
    useJUnitPlatform()
}

 

주의할 점은 스프링 AOP 라이브러리도 추가해야 된다는 점입니다.

resilience4j를 스프링 부트에서 사용할 때 메서드 레벨의 애너테이션을 주로 사용하는데 이 과정에서 AOP가 사용되기 때문입니다.

 

디펜던시들을 살펴보면 resilience4j 관련 디펜던시가 두 개가 있습니다.

사실 implementation "io.github.resilience4j:resilience4j-spring-boot2"만 있어도 동작합니다.

오히려 부트 스타터에서 제공하는 디펜던시만 땡길 경우, application.yml 에서 설정을 해줄 수가 없었습니다.

여기서는그냥 속편하게 두 개 다 땡기고 진행하겠습니다.


서킷 브레이커 초기 설정을 작성합니다.

 

application.yml

resilience4j.circuitbreaker:
  configs:
    default:
      failureRateThreshold: 50
      slowCallRateThreshold: 100
      slowCallDurationThreshold: 60000
      permittedNumberOfCallsInHalfOpenState: 4
      maxWaitDurationInHalfOpenState: 1000
      slidingWindowType: COUNT_BASED
      slidingWindowSize: 10
      minimumNumberOfCalls: 10
      waitDurationInOpenState: 10000
  instances:
    hello:
      baseConfig: default

위처럼 값들을 지정해두면 CircuitBreakerRegistry에 값이 저장됩니다.

CircuitBreakerRegistry는 설정값을 저장하는 인메모리 저장소라고 생각하면 됩니다. 위에서는 "default 설정값 세트를 저장해두고 이 값들을 이용해 hello라는 이름의 서킷브레이커 인스턴스를 만들겠다" 라는 의도로 이해하면 됩니다.

이렇게하면 나중에 코드에서 hello라는 이름으로 위 설정이 적용된 CircuitBreaker를 생성할 수 있습니다.

 

몇 가지 프로퍼티들을 살펴보겠습니다.

 

failureRateThreshold => 실패 비율이 이 값을 넘어가면 회로를 차단합니다.

permittedNumberOfCallsInHalfOpenState => 회로가 반-개방 상태일 때, 받아들일 요청의 개수를 지정합니다.

maxWaitDurationInHalfOpenState => 회로가 반-개방 상태일 때, 처리하는 요청의 최대 타임아웃입니다.

slidingWindowType => 슬라이딩 윈도우를 요청의 개수  or 시간으로 지정할 수 있습니다. 간단한 테스트를 위해 개수로 지정합니다.

minimumNumberOfCalls => 실패율과 느린 응답 비율을 계산할 최소 요청 수를 지정합니다. 만약 이 값이 10이라면 9개의 요청이 모두 실패하더라도 회로는 닫히지 않습니다. 10개가 되지 않아 실패율과 느린 응답 비율을 계산하지 않았기 때문입니다.

 

해당 프로퍼티들에 대한 더 자세한 설명은 아래에서 참고하세요.

 

 

CircuitBreaker

Getting started with resilience4j-circuitbreaker

resilience4j.readme.io

 

이번 테스트 설정에 중요한 값은 failureRateThreshold,  slidingWindowType, minimumNumberOfCalls 입니다.

 

이제 10개의 요청을 검사하고 실패율이 50프로가 넘어갈 경우 회로가 오픈되는지 테스트해보겠습니다.


간단하게 컨트롤러와 서비스를 만듭니다.

 

HelloRestController

@RestController
public class HelloRestController {

    private final HelloService helloService;

    public HelloRestController(HelloService helloService) {
        this.helloService = helloService;
    }

    @GetMapping("/hello")
    public String hello() {
        return helloService.greeting();
    }
}

HelloService

package springboot.cloud.circuitbreaker.service;

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;

import java.util.Random;

@Service
public class HelloService {

    @CircuitBreaker(name = "hello", fallbackMethod = "helloFallback")
    public String greeting() {
        randomException();
        return "hello world!";
    }

    private void randomException() {
        int randomInt = new Random().nextInt(10);

        if(randomInt <= 7) {
            throw new RuntimeException("failed");
        }
    }

    private String helloFallback(Throwable t) {
        return "fallback invoked! exception type : " + t.getClass();
    }

}

@CircuitBreaker 의 파라미터에 application.yml 에서 지정했던 인스턴스 이름인 hello를 지정했습니다.

그리고 fallbackMethod도 지정합니다. fallback 메서드 지정은 메서드 이름을 이용하고, 폴백 메서드는 반드시 @CircuitBreaker 지정 메서드와 같은 클래스에 위치해야합니다.

 

무작위로 0~9까지 수를 생성하고, 그 수가 7이하면 예외를 던져 실패하도록 합니다. 이러면 50프로 이상의 확률로 실패하겠죠?

실패시에는 폴백 메서드를 이용해 메세지를 반환하도록 합니다. 메세지는 예외의 타입을 붙여 반환합니다.

 

이렇게하는 이유는 회로가 열려 호출이 차단될 때와 실패할 때의 예외가 다르기 때문입니다.


자! 이제 서버를 실행시키고 /hello 를 10번 호출해봅니다.

 

10번 이전까지는 예상대로 RuntimeException 예외가 발생했습니다.

 


하지만 시도가 10번이 넘어가면...

 

서킷브레이커에서 CallNotPermittedException이 던져진 걸 확인할 수 있네요.

 

예상대로 실패율이 제한을 초과하자 회로가 오픈돼, 메서드 호출 이전에 차단된 걸 확인할 수 있습니다.

Comments