CircuitBreaker
회로차단기를 뜻으로 (금융·주식 파동에 대비한) 거래 일시 중지 (조치), 안전 장치. 라는 뜻도 가지고 있다.
프로그래밍에서도 비슷한 의미로 사용되며 API를 통한 외부 서비스와의 통신이 많은 MSA 환경에서 반드시 필요한 개념이다.
예를 들어 A서비스에서 B서비스의 API를 사용하고 있는데 B서비스에서 장애가 발생했다고 가정해보자. 장애에는 여러가지 상황이 있을 수 있는데 대표적으로 서비스가 아예 죽었거나 갑자기 트래픽이 몰려 처리 시간이 오래 걸리는 경우를 생각해볼 수 있다.
타겟 서비스가 아예 죽어있는 상황이라면 그나마 나은 편에 속한다. 일반적으로 호출하는 서비스에서는 이러한 상황에 대해 예외처리를 해놓았을 것이기 때문이다. 별도의 공통 에러 페이지를 띄우는 것이 바로 그것이다.
하지만 갑자기 트래픽이 몰려 처리시간이 오래 걸리는 상황이라면 호출하는 쪽에서는 이를 에러로 판단하기 위해 지정된 timeout 시간동안 대기를 해야 하고 공통 에러페이지도 그때서야 띄워줄 수 있다.
타겟 서비스 역시 아직 처리하지 못한 일이 있는데 지속적으로 트래픽이 적재되어 처리시간은 더 오래 걸리게 되고 결국 서비스가 죽는 장애로 이어지게 된다.
이를 방지하기 위해 등장한 것이 CircuitBreaker 패턴으로 A서비스와 B서비스 사이에 CircuitBreaker 를 두어 B서비스가 장애 상황이라고 판단된다면 일정시간동안 연결을 끊고 자체적인 Fallback 메시지를 리턴하는 패턴이다.
Resilience4j
위에서 말한 CircuitBreaker 패턴을 구현하기 위한 라이브러리는 대표적으로 Netflix 의 Hystrix 가 있는데 더이상 개발을 진행하지 않고 운영모드로 전환된 상태로 Resilience4j 를 사용할 것을 권하고 있는 상황이다.
Resilience4j는 Hystrix 로 부터 영감을 받은 라이브러리로 Hystrix 에 비해 다른 라이브러리 의존성이 적어 사용하기 가볍다는 장점이 있고 SpringBoot 환경에 적용하고자 한다면 resilience4j-spring-boot 또는 spring-cloud-starter-circuitbreaker-resilience4j 를 사용하면 되고 함께 제공되는 어노테이션을 통해 AOP 방식으로 간편하게 CircuitBreaker 패턴을 구현할 수 있다.
Resilience4j에서는 아래와 같이 5개의 코어모듈을 제공한다.
CircuitBreaker
Retry
RateLimiter
Bulkhead
TimeLimiter
resilience4j-spring-boot 기준 구현방법은 아래를 참고하자. spring-cloud-starter-circuitbreaker-resilience4j 를 사용하더라도 구현방법이 크게 다르지는 않다.
https://leejongchan.tistory.com/100
Resilience4j 의 코어 모듈들은 어노테이션으로 특정 요청에 모두 한번에 적용 가능하고 이 경우 기본적인 적용 순서는 아래와 같다.
Bulkhead -> TimeLimiter -> RateLimiter -> CircuitBreaker -> Retry
이는 아래와 같이 설정된 기본 order 값을 따른다.
type | order default value |
Retry | Ordered.LOWEST_PRECEDENCE - 4 |
CircuitBreaker | Ordered.LOWEST_PRECEDENCE - 3 |
RateLimiter | Ordered.LOWEST_PRECEDENCE - 2 |
TimeLimiter | Ordered.LOWEST_PRECEDENCE - 1 |
Bulkhead | Ordered.LOWEST_PRECEDENCE |
만약 적용순서를 변경하고 싶다면 application.yml 에서 설정하는 기준 아래 값을 적절히 조정하면 된다.
- resilience4j.retry.retryAspectOrder
- resilience4j.circuitbreaker.circuitBreakerAspectOrder
- resilience4j.ratelimiter.rateLimiterAspectOrder
- resilience4j.timelimiter.timeLimiterAspectOrder
- resilience4j.bulkhead.bulkheadAspectOrder
예를 들어 만약 CircuitBreaker 전에 Retry 를 적용하고 싶다면 Retry 의 order 값을 CircuitBreaker 보다 높게 설정하면 된다.(order 가 높은게 먼저 수행됨) 이 경우 Retry 의 maxAttempts 가 3이라면 3번의 retry 후 CircuitBreaker 에 1회 fail 로 기록된다.
resilience4j 설정은 기본적으로 application.yml 파일에서 모두 가능하지만 필요에 따라 javaConfig 를 통해 커스터마이징을 진행할수도 있고 이를 위해 각 Bean을 생성할 수 있는 Builder 를 제공한다.
Resilienc4j Type | Instance Customizer class |
Circuit breaker | CircuitBreakerConfigCustomizer |
Retry | RetryConfigCustomizer |
Rate limiter | RateLimiterConfigCustomizer |
Bulkhead | BulkheadConfigCustomizer |
ThreadPoolBulkhead | ThreadPoolBulkheadConfigCustomizer |
Time Limiter | TimeLimiterConfigCustomizer |
아래와 같이 사용하면 된다.
@Configuration
public class Resilience4jConfig {
@Bean
public CircuitBreakerConfigCustomizer testCircuitBreaker() {
return CircuitBreakerConfigCustomizer
.of("testCircuitBreaker", builder -> builder.slidingWindowSize(100));
}
}
TimeLimiter 는 제한적으로 적용할 수 있다. 리액티브 통신에만 적용?
https://github.com/resilience4j/resilience4j/issues/849
SpringBoot 1.5.x 에서 RestClient 모듈 선택
많이들 사용하는 RestClient 모듈은 RestTemplate, Feign, WebClient 가 있다. 하지만 RestTemplate 는 Spring에서 권장하는 방식이 아니고 곧 deprecated 될 예정이기 때문에 새롭게 RestClient를 구성한다면 선택할 이유가 없다.
그 대안으로 Feign, WebClient 가 있다. Spring5를 사용한다면 고민할 것도 없이 WebClient를 사용하겠지만 Spring4를 사용하는 SpringBoot 1.5.x를 사용하는 레거시 프로젝트에서는 Feign을 사용해야 한다.
하지만 Feign 을 사용하기로 했다고 하더라도 다른 산이 남아있다. 바로 CircuitBreaker 적용이다. CircuitBreaker 구현체인 Resilience4j 를 FeignClient 에 적용하려면 resilience4j-feign 라이브러리를 사용해야 하는데 이 경우 Feign Contract 는 Feign.Default 로 강제된다.
FeignClient 인터페이스를 작성할 때 Spring에서 사용하던 RequestMapping를 사용할 수 없고 RequestLine 같은 Feign.Default 에서 제공하는 어노테이션을 사용해야 한다는 말이다.
물론 spring-cloud-starter-openfeign 로 제공되는 OpenFeign 을 사용하고 resilience4j-spring-boot 또는 spring-cloud-starter-circuitbreaker-resilience4j 를 사용하면 FeignClient 에서 RequestMapping을 사용하면서 Resilience4j 를 적용할 수도 있다.
그런데 알다시피 spring-cloud의 경우 SpringBoot 버전마다 적용할 수 있는 것이 지정되어 있고 SpringBoot1.x 버전대에서 사용할 수 있는 spring-cloud 에는 spring-cloud-starter-circuitbreaker-resilience4j 가 포함되어 있지 않다.
또한 resilience4j-spring-boot 를 사용할 경우 Resilience4j 관련 설정을 application.yml 에 해야 하는데 일부 설정값을 Duration 타입으로 받고 있고 application.yml 파일에서 Duration 타입 컨버팅은 2.1 버전 부터 지원하기 때문에 아래와 같은 에러를 마주하게 된다.
Failed to convert property value of type 'java.lang.Integer' to required type 'java.time.Duration'
이런 이유로 SpringBoot 1.5.x 에서 Feign과 Resilience4j 를 적용하려면 spring-cloud-starter-openfeign, resilience4j-spring-boot 의존성을 사용하되 application.yml 이 아닌 javaConfig 로 Resilience4j.CircuitBreaker 를 적용해야 한다.
이렇게 하더라도 마지막 문제가 남아있다. 바로 FeignClient에 CircuitBreaker 를 연동하는 일이다. 최근 starter-circuitbreaker-resilience4j 를 사용한다면 아래와 같이 FeignClient 인터페이스에서 resilience4j 주석을 직접 적용할 수 있고 fallback 메서드 또한 default 메서드로 정의하면 정상 동작한다.https://github.com/resilience4j/resilience4j/issues/645
하지만 SpringBoot 1.5.x 버전에서 지원되는 spring-cloud 에서는 이러한 기능이 지원되지 않기 때문에 아래 포스팅 처럼 FeignClient를 사용하는 별도 Wrapper 클래스를 만들어서 그곳에 resilience4j 를 적용해야 한다.https://nnoco.tistory.com/297
Feign 은 내부적으로 ApacheHttpClient 를 사용하는데 일반적으로 이를 OkHttpClient 로 변경해서 사용한다.
https://github.com/square/okhttp/issues/3472
마지막으로 Feign은 기본적으로 http connection pool 을 사용하지 않는데 이를 적용하기 위해서는 feign-okhttp 의존성을 추가하고 @Configuration 클래스에 아래와 같은 OkHttpClient Bean을 정의해 놓아야 한다. 참고로 OkHttpClient 정의시 타임아웃을 지정할 필요가 없다. 지정하더라도 Feign의 설정으로 강제로 변경된다.
@Bean
public okhttp3.OkHttpClient okHttpClient(){
return new okhttp3.OkHttpClient.Builder()
.connectionPool(new ConnectionPool(5, 5, TimeUnit.MINUTES))
.build();
}
이때 주의할 점은 SpringBoot 1.5.x 를 사용할 경우 절대 feign-okhttp 의존성 최신버전을 사용하면 안된다는 점이다. 위에서 말했듯이 SpringBoot의 경우 사용할 수 있는 spring-cloud 버전이 지정되어 있고 그곳에서 제공되는 Feign의 버전과 feign-okhttp 에서 제공하는 feign-okhttp 이 호환되지 않을 수 있다.
내가 확인한 바로는 feign-okhttp 9.3.1 은 SpringBoot 1.5.x 에서 사용할 수 있는 spring-cloud Edgware.SR6 버전과 무리없이 호환되었다.
'Backend > Spring+Boot' 카테고리의 다른 글
@RunWith(SpringRunner.class) (0) | 2021.07.13 |
---|---|
@Async 설정 (0) | 2021.07.13 |
ObjectMapper json 직렬화/역직렬화 주의사항 (0) | 2021.07.07 |
@RequestMapping consumes, produces (0) | 2021.07.07 |
SpringBoot Redis Cluster Lettuce 설정 (0) | 2021.05.04 |