Backend/Kotlin

[Kotlin] SpringMVC WebClient 에 Resilience4j 적용하기

findmypiece 2022. 5. 10. 13:55
728x90

WebClient 는 SpringMVC 에서 동기식으로 사용할 것이고 WebClient가 아닌 다른 rest client 를 선택하더라도 적용방법은 크게 달라지지 않는다. WebFlux 에서 사용하는 비동기식 WebClient 에 적용하는 방법은 추후 다시 정리하도록 한다.

 

Resilience4j 에서는 여러가지 모듈을 제공하는데 여기에서는 일반적으로 많이 사용하는 retry, circuitbreaker 모듈 적용만 정리한다. 

retry, circuitbreaker 설정에는 여러가지 설정값들이 존재하겠지만 application.yml 에 아래와 같이 설정되어 있다고 가정해보자.

 

resilience4j:
  retry:
    configs:
      default:
        maxRetryAttempts: 3
        waitDuration: 500ms
        failAfterMaxRetries: true
        ignoreExceptions:
          - com.melon.inms.batch.common.exception.InmsRestClientException
    instances:
      default:
        baseConfig: default

  circuitbreaker:
    configs:
      default:
        registerHealthIndicator: true #actuator 에서 CircuitBreaker 상태확인을 위해 필요
        failureRateThreshold: 50 #circuitBreaker open 할 에러 비율
        slowCallDurationThreshold: 30s #응답시간이 느린것으로 판단할 기준 시간
        slowCallRateThreshold: 50 #circuitBreaker 를 open 할 응답시간이 느린 비율
        minimumNumberofCalls: 10 #circuitBreaker 를 open 여부를 판단하기 위해 비율을 계산할 최소 호출 수
        waitDurationInOpenState: 60s #circuitBreaker가 half open 으로 전환되기 전 유지시간
        permittedNumberOfCallsInHalfOpenState: 3 #half open 상태에서 open or close 결정을 위해 비율을 계산할 호출 수
        ignoreExceptions:
          - com.melon.inms.batch.common.exception.InmsRestClientException
    instances:
      default:
        baseConfig: default

retry 설정은 너무 직관적이라 넘어가고 circuitbreaker 설정에 대해 살펴보자. 일단 동작방식을 간단하게 정의하면 close -> open -> fallback -> half open -> close or open 와 같은 순서로 진행된다. 위 설정 기준으로 동작방식을 나열하면 아래와 같다.

  1. circuitbreaker.configs.default 를 통해 circuitbreaker 공통옵션을 지정한다.
  2. 최소 minimumNumberofCalls 번의 호출이 있은 뒤에 circuitbreaker open 이 검토된다.
  3. minimumNumberofCalls 번의 호출 중 에러율이 failureRateThreshold 이상이면 circuitbreaker가 open 된다.
  4. minimumNumberofCalls 응답시간이 slowCallDurationThreshold 이상인 호출비율이 slowCallRateThreshold 이상이면 circuitbreaker가 open 된다.
  5. open 된 circuitbreaker 는 waitDurationInOpenState 동안 유지된다.
  6. waitDurationInOpenState 이 지난 뒤에 circuitbreaker 는 half open 상태가 된다.
  7. half open 상태에서 permittedNumberOfCallsInHalfOpenState 번의 호출을 처리해서 2, 3번 체크를 한 뒤에 다시 open 또는 close 여부를 결정한다.
  8. ignoreException 로 지정된 Exception 은 circuitbreaker open 또는 close 비율에 아무런 영향을 주지 않는다.
  9. default 라는 circuitbreaker 인스턴스를 생성해서 공통옵션을 적용한다.

이렇게 설정한 circuitbreaker 는 코드상 아래와 같이 사용할 수 있다.

@Retry(name = "default")
@CircuitBreaker(name = "default", fallbackMethod = "getPushTrgtCnt")
fun getPushTrgtCnt(): Long {
    return runBlocking {
        client.get()
            .uri("...")
            .retrieve()
            .awaitBody()
    }
}

private fun getPushTrgtCnt(e: CallNotPermittedException): Long {
    logger.error("defaultCircuitBreakerFallback: ${e.message}")
    return 0L
}

 

retry, circuitbreaker 가 지정된 메소드에서 예외가 발생시 기본적으로 예외를 throw 하고 fallbackMethod 가 지정되어 있을 경우 발생된 예외와 함께 그곳으로 이동한다.

 

retry, circuitbreaker 조건(maxRetryAttempts, failureRateThreshold) 에 부합할 경우 fallbackMethod 로 이동되기 전 지정된 동작을 수행한다. 단, 발생된 예외가 ignoreExceptions 에 명시된 예외라면 retry, circuitbreaker 처리 없이 곧바로 fallbackMethod 로 이동한다.

 

fallbackMethod 는 선택사항이지만 이러한 상황을 인지할 수 있도록 되도록 지정해서 최소한 위처럼 로그를 남기거나 별도 채널로 알림을 보내는 것이 좋다.

 

특히 retry fallback 의 경우 일시적일 수 있지만 circuitbreaker fallback 의 경우 최소한 waitDurationInOpenState 동안 지속적으로 호출이 실패할 것이고 해당 서버에 문제가 있는 상황이기 때문에 반드시 알림을 보내야 한다.

 

이런 이유로 일반적으로 위와 같이 retry 는 fallbackMethod 를 지정하지 않아 호출한 곳으로 예외를 전달하고 circuitbreaker 는 fallbackMethod 를 지정해서 적절한 처리를 하도록 한다.

 

fallbackMethod 는 retry, circuitbreaker 적용된 본래 메소드의 인자와 Exception 인자를 포함해야 하며 리턴타입 역시 본래 메소드와 일치해야 한다.

 

circuitbreaker fallbackMethod 인자에 포함할 Exception 은 반드시 CallNotPermittedException 타입이어야 한다. 그렇지 않으면 retry, circuitbreaker 정책과 무관하게 예외발생시 무조건 해당 fallbackMethod 를 타게된다.

 

물론 retry 로 fallbackMethod 가 호출될 경우에는 실제 예외가 발생한 Exception 을 받게되고 circuitbreaker 가 open 된 상태에서는 CallNotPermittedException 를 받게된다.

 

이에 fallbackMethod 내부에서 Exception 파라미터의 타입을 체크해서 적절한 처리를 해도 되지만 정확하게 circuitbreaker 가 open 된 상태에서만 fallbackMethod 가 호출되게 하고 싶다면 CallNotPermittedException 타입을 인자로 받도록 해야 한다.

 

만약 fallbackMethod 에서 리턴할 적절한 default 값이 없고 실패로 처리하고 싶은 경우 Exceptin 자체를 throw 하면 된다.

 

이렇게 fallbackMethod 에서 Exception 자체를 throw 할 경우 결국 본래 메소드에서도 Exception 이 발생하는 것이기 때문에 circuitbreaker 가 open 상태라 할지라도 retry 는 동일하게 수행된다.

 

circuitbreaker 에 의해 실제 메소드에 진입은 되지 않고 곧바로 circuitbreaker fallbackMethod 메소드가 호출되겠지만 지정된 retry 횟수만큼 circuitbreaker fallbackMethod 가 호출된다.

 

이는 불필요한 오버헤드로 이러한 현상을 막으려면 retry ignoreExceptions 에 CallNotPermittedException 를 지정하면 된다.

 

https://sabarada.tistory.com/205
https://arnoldgalovics.com/resilience4j-webclient/
https://otrodevym.tistory.com/entry/spring-boot-설정하기-24-spring-cloud-Resilience4j1-설정-및-테스트-소스
https://dlsrb6342.github.io/2019/06/03/Resilience4j란/
https://rusyasoft.github.io/java/2020/03/20/Usage-resilience4j-retry-cb/
https://jydlove.tistory.com/72
https://github.com/resilience4j/resilience4j/issues/1236
728x90