코딩 하는 가든

Spring Webflux에 Resilience4j를 이용해 서킷브레이커 패턴 구현하기 - (2) 본문

Spring (boot)

Spring Webflux에 Resilience4j를 이용해 서킷브레이커 패턴 구현하기 - (2)

가든리 2021. 5. 5. 15:54

이번 글에선 실제로 코드를 보며 어떻게 서킷브레이커를 구현 하는지 알아 보겠습니다.

 

예제에 사용된 코드는 여기에서 확인 가능 합니다.

 

우선 Resilience4j는 Spring MVC와 Spring Webflux 환경에서 모두 사용 가능 합니다. 이번 예제에서 Webflux를 선택 한 이유는 MSA환경에서 많은 api 호출이 일어나는 환경에서는 Webflux가 많이 유리 할 수 있고, 개인적으로 느끼기에 점점 Webflux를 이용하여 개발을 많이 해 가고 있다고 생각 하기 때문 입니다.

 

물론 Webflux가 만능은 아니며 토비의 스프링의 저자이신 토비님 께서는 if kakao에서 MVC 환경 에서 문제 없이 애플리케이션이 돌아 간다면 굳이 Webflux를 선택 할 이유가 없다고 하셨습니다.

https://tv.kakao.com/channel/3150758/cliplink/391418995

 

물론 서킷브레이커의 도입을 고민 하시고 계신 분이라면 라이브러리 선택은 이미 끝났다고 생각 하겠습니다 .. 

 

서킷 브레이커 구현

Webflux + Resilience4j를 사용하기 위해서는 다음과 같은 라이브러리 의존성을 추가 해야 합니다.

implementation 'org.springframework.boot:spring-boot-starter-webflux'   // Webflux
implementation 'org.springframework.boot:spring-boot-starter-aop'       // Resilience4j의 어노테이션 방식을 사용하기 위해 필요
implementation "io.github.resilience4j:resilience4j-spring-boot2:1.7.0" // Resilience4j
implementation 'io.github.resilience4j:resilience4j-reactor:1.7.0'      // Reactive 환경에서 Resilience4j를 사용 하기 위해 필요

 

글 쓰는 기준 현재는 1.7.0 버전이 최신이며 최신 버전은  여기 에서 확인 가능 합니다.

 

 

먼저 외부 Api를 호출 하는 컴포넌트를 만듭니다.

@Component
public class TestApi {

    private WebClient webClient;

    TestApi() {
        webClient = WebClient.builder().baseUrl("http://localhost:8081").build();
    }

    public Mono<String> getApiData(String key) {
        return webClient.get()
                .uri("/data?key=" + key)
                .retrieve()
                .bodyToMono(String.class);
    }
}

Webclient를 이용해 api를 호출 하도록 했습니다. 테스트를 위해 localhost:8081에 테스트를 위한 API 서버를 띄워 놓았습니다.

 

API 서버는 간단하게 key라는 파라미터에 fail이 들어오면 exception을 반환 하도록 만들었습니다.

/**
 * 8081에 떠있는 API 서버
 * */
@RestController
public class DataController {

    @GetMapping("/data")
    public String success(String key) {
        if (key.equals("fail")) {
            throw  new RuntimeException();
        }
        return "data";
    }
}

 

이렇게 API 호출부와 API서버가 준비 되면 서킷 브레이커를 적용 해 볼 수 있는 준비가 끝났습니다.

 

Resilience4j의 서킷 브레이커를 정의 해 보겠습니다. 서킷 브레이커를 정의 할 수 있는 방법에는 두 가지가 있습니다.

1. Java 파일로 빈을 정의 한다.

2. yml, properties 파일에 정의 한다.

 

먼저 1번 방식 입니다.

@Configuration
public class CircuitConfig {

    @Bean // 1
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        return CircuitBreakerRegistry.of(
                CircuitBreakerConfig.custom() // 2
                    .slidingWindowSize(10) // 3
                    .failureRateThreshold(80) // 4
                    .build()
        );
    }

    @Bean // 5
    public CircuitBreaker circuitBreaker(CircuitBreakerRegistry circuitBreakerRegistry) {
        return circuitBreakerRegistry.circuitBreaker("garden");
    }
}
  1. 서킷 브레이커 레지스트리를 위한 빈을 정의 합니다. 서킷 브레이커 레지스트리는 서킷 브레이커 객체들을 담고 있는 컨테이너 입니다.
  2. 서킷 브레이커 레지스트리에서 생성 될 서킷브레이커의 설정을 정의 합니다.
  3. 서킷 브레이커는 최근 요청 중 slidingWindowSize만큼의 요청을 저장 합니다.
  4. 서킷 브레이커는 slidingWindow에 저장된 call의 성공, 실패 여부를 저장 하여 실패 확률이 failureRateThreshold에 도달 하면 서킷 브레이커를 OPEN상태로 만들고 호출을 차단 합니다.
  5. 서킷 브레이커 레지스트리를 통해 garden이라는 이름의 서킷 브레이커를 만듭니다.
    circuitBreaker("name") 메소드는 해당 이름의 서킷브레이커가 없으면 새로 생성 해 주고 있다면 해당 객체를 반환 합니다.

다음으로 2번 방식으로 yml에 정의 하는 방식 입니다.

resilience4j:
    circuitbreaker:
        configs:
            default:
                slidingWindowSize: 10
                failureRateThreshold: 80

        instances:
            garden:
                baseConfig: default

instances에 만들고자 하는 서킷 브레이커의 이름과 어떤 설정을 가질지를 정의 해 줄 수 있습니다.

 

주의할 점

2번 yml방식을 사용 하면CircuitBreaker 자체가 Bean으로 등록 되지 않는다는 것 입니다. Spring Bean에 등록 되는 것은 CircuitBreakerRegistry뿐이며 서킷 브레이커 객체를 이용 하기 위해서는 registry에서 직접 꺼내 주어야 합니다.

 

이렇게 서킷 브레이커를 정의 했으면 이 서킷 브레이커를 이용해 API 호출을 차단 해 보겠습니다.

서킷 브레이커를 손 쉽게 사용 하는 방법은 어노테이션 방식을 이용 하는 것 입니다.

@RequiredArgsConstructor
@Service
public class ServiceA {

    private final TestApi apiA;

    public Mono<String> getMonoData(String key) {
        return apiA.getApiData(key);
    }
}

 

위에 정의한 TestApi 컴포넌트를 통해 api를 호출 하는 서비스를 정의 해 보겠습니다.

이 서비스의 getMonoData 메소드에 서킷 브레이커를 적용 하는 방법은 간단하게 @CircuitBreaker 어노테이션을 붙여 주는 것 뿐 입니다.

@RequiredArgsConstructor
@Slf4j
@Service
public class ServiceA {

    private final TestApi apiA;

    @CircuitBreaker(name = "garden", fallbackMethod = "fallback")
    public Mono<String> getMonoData(String key) {
        return apiA.getApiData(key);
    }

    public Mono<String> fallback(String key, Throwable t) {
        log.error("Fallback : " + t.getMessage());
        return Mono.just("fallback data");
    }
}
  1. @CircuitBreaker 어노테이션을 서킷브레이커를 적용시키고 싶은 메소드 위에 붙입니다.
  2. 어노테이션의 인자는 name (서킷브레이커의 이름) 과 fallbackMethod(Exception 발생 시 수행 할 메소드) 입니다.
  3. fallback메소드를 정의 할 때는 주의 할 점이 있는데 fallback의 파라미터로 서킷을 정의 한 메소드의 파라미터를 똑같이 정의 하고 어떤 Exception을 잡을 것인지를 하나 더 써줘야 한다는 것 입니다.
    getMonoData(String key)
    fallback(String key, Throwable t)
    저는 Throwable을 인자로 주었기 때문에 모든 타입의 익셉션을 처리 할 수 있고, 메소드 오버로딩을 통해 여러 폴백을 정의 하면 가장 가까운 타입의 익셉션 인자가 있는 메소드를 수행 하게 됩니다.

이렇게 하고 서비스를 호출 하는 컨트롤러를 만들어 보겠습니다.

@RequiredArgsConstructor
@RestController
public class TestController {

    private final ServiceA serviceA;

    @GetMapping("/annotation")
    public Mono<String> testAnnotation(String key) {
        return serviceA.getMonoData(key);
    }
}

 

이렇게 해서 애플리케이션을 시작 하고 요청을 보내 보겠습니다.

먼저 slidingWindow(10)를 꽉 차게 하기 위해

http://localhost:8080/annotation?key=success 라는 url로 10번 호출을 한 후 

http://localhost:8080/annotation?key=fail로 10 번을 호출 해 보겠습니다.

에러 로그를 보면 failureRateThreshold가 도달 하기 까지 8번의 호출은 실제 api를 호출 해서 Exception이 발생 했지만 80%가 넘어간 순간 부터 CircuitBreaker의 상태가 OPEN이 되어 API의 호출을 완전히 차단 한 것을 볼 수 있습니다.

서킷 브레이커가 OPEN된 상태에서 메소드를 호출 하면 CallNotPermittedException이 발생 하며 호출을 막게 됩니다.

 

이렇게 OPEN 상태가 된 서킷은 시간이 지나면 HALF_OPEN상태 ( 이 때 약간만 호출을 해서 다시 서킷을 열지 정상 상태로 되돌릴지 판단 합니다.)를 거쳐 다시 OPEN이 되거나 CLOSE 상태가 될 수 있습니다.

 

이 때 몇 초간 기다려서 서킷의 상태를 바꿀 것인지 등에 대한 세세한 설정을 해 줄 수 있는데 상세 설정은 여기를 참고해 보시면 됩니다. 이 외에도 slowCall이 발생 했을 때도 서킷 브레이커를 발동 시킬 수 있으며 설정을 통해 더 세세한 컨트롤이 가능 해 집니다.

 

 

어노테이션을 이용해 서킷 브레이커를 사용 하는 방법 외에도 서킷브레이커 오퍼레이션을 통해도 사용 할 수 있습니다.

이렇게 사용 할때는 주로 서킷브레이커를 Spring Bean이 아니라 순수한 자바 객체에서 사용 하고 싶을 때가 있습니다.

어노테이션 방식을 사용하려면 Spring AOP를 이용 해야 하는데 이는 Spring Bean이 아니면 동작 하지 않기 때문 입니다.

public class ServiceB {

    private TestApi api;
    private CircuitBreakerRegistry circuitBreakerRegistry;
    
    public ServiceB(TestApi api, CircuitBreakerRegistry registry) {
        this.api = api;
        this.circuitBreakerRegistry = registry
    }

    public Mono<String> getMonoData(String key) {
        CircuitBreaker circuit = circuitBreakerRegistry.circuitBreaker("garden");

        return api.getApiData(key)
                .transform(CircuitBreakerOperator.of(circuit))
                .onErrorResume(this::fallback);
    }

    public Mono<String> fallback(Throwable t) {
        log.error("Fallback : " + t.getMessage());
        return Mono.just("fallback data");
    }
}

 

위와 같이 Reactive Stream 중간에 .transform 메소드 체인을 이용 하여 CircuitBreakerOperator.of(circuitBreaker) 를 이용 하면 transform 메소드의 업스트림에 대해 서킷 브레이커 적용이 가능 합니다.

api.getApiData(key) 의 과정에서 익셉션이 난것을 서킷브레이커 오퍼레이션이 캐치 하여 서킷 브레이커를 발동 시켜 줄 것 입니다.

마찬가지로 서킷이 오픈 되면 CallNotPermittedException이 발생 하게 되고 그 익셉션을 onErrorResume으로 캐치해 처리 하는 전략 입니다.

 

Resilience4j에는 이런 서킷 브레이커 이외에도 장애에 강한 시스템을 만들기 위한 TimeLimiter, Retry같은 여러 모듈들이 존재 합니다. 시스템을 더 안전하게 만들고 싶은 분이라면 Resilience4j의 다른 모듈에 대해서도 알아 보시길 권합니다.

github.com/resilience4j/resilience4j

 

resilience4j/resilience4j

Resilience4j is a fault tolerance library designed for Java8 and functional programming - resilience4j/resilience4j

github.com