본문 바로가기

spring

reseilence4j를 이용한 장애 대처

들어가면서

데이터 베이스나 외부 API 서버에서 장애가 발생하면 개발한 서버에서도 역시 장애가 발생할 수 있다. 특히 외부 API를 이용할 때는 외부의 시스템 장애를 고칠 수 있는 방법이 없는 경우도 존재할 것이다. 이런 장애 상황을 대체하기 위해서는 어떻게 해야 할까? 그런 상황에서 reseilence4j 같은 라이브러리를 이용하면 쉽게 장애에 대응하는 코드를 작성할 수 있다.

reseilence4j란?

Netflix Hystrix에서 영감을 받아서 개발된 가벼운 결함 관리 라이브러리이다. Circuit breaker, Retry, Bulkhead, RateLimiter, TimeLimiter 같은 솔루션을 제공하고 있다.

Core Modules (핵심 모듈)

다섯 가지의 핵심 모듈을 제공하고 있는데 각 모듈이 제공하는 기능에 대해서 알아보자.

Circuit breaker

두꺼비집이 내려가는 상황은 전류가 누전되어서 감전 사고가 발생할 위험이 있는 경우에 두꺼비집(누전 차단기)이 내려가서 사고를 미연에 방지한다.
두꺼비집처럼 사고 상황을 격리할 수 있는 패턴을 circuit breaker 패턴이라고 하는데 이 패턴을 쉽게 적용할 수 있는 모듈을 제공한다.

언제 사용할까?

외부 시스템(API 서버, DB 등)의 장애가 실행되는 서버의 장애(실패)로 이어지지 않기를 원할 때, 사용하면 좋다.

어떻게 사용할까?

사용방식은 두가지 방식이 존재하는데 하나는 메서드 체인을 이용한 Fluent api를 사용하는 방식과 annotation을 이용한 방식이다. 제공하는 적용 방식은 resilience4j가 제공하는 모든 코어 모듈에서 동일하다.

Fluent API 기반
CircuitBreaker 생성

CircuitBreakerRegistry를 통해서 등록한 CircuitBreaker를 찾아서 사용할 수 도 있다.

// Create a custom configuration for a CircuitBreaker
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
    .failureRateThreshold(50)
    .slowCallRateThreshold(50)
    .waitDurationInOpenState(Duration.ofMillis(1000))
    .slowCallDurationThreshold(Duration.ofSeconds(2))
    .permittedNumberOfCallsInHalfOpenState(3)
    .minimumNumberOfCalls(10)
    .slidingWindowType(SlidingWindowType.TIME_BASED)
    .slidingWindowSize(5)
    .recordExceptions(IOException.class, TimeoutException.class)
    .build();

// Create a CircuitBreakerRegistry with a custom global configuration
CircuitBreakerRegistry circuitBreakerRegistry =
    CircuitBreakerRegistry.of(circuitBreakerConfig);

Optional<CircuitBreaker> name1 = circuitBreakerRegistry.find("name1");

CircuitBreaker의 팩토리 메서드를 이용해서 생성도 가능하다.

CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
Circuit breaker 적용하기

생성된 Circuit breaker를 사용하는 방법은 Decorators 유틸리티가 제공하는 빌더를 이용하는 방법과 CircuitBreaker가 제공하는 빌더를 이용하는 방법이 있다.

circuitBreaker.executeSupplier(() -> "hello");

단순히 circuit breaker를 적용하기 위해서는 executeXXX를 적용하면 circuit breaker를 적용하게 된다.

하지만 다른 기능을 중첩해서 사용하고 싶다면 그리고 fallback처리나 성공, 실패 시 이벤트를 핸들링하고 싶다면 이런 단순한 방법으로는 한계가 있다.

circuitBreaker.decorateSupplier(() -> "hello");

추가적인 기능을 데코레이트하기 위해서는 decorateXXX를 사용해야한다. 반환값으로는 그림처럼 지정한 함수형 인터페이스를 반환하게 되는데 이런 함수 인터페이스를 사용해서 그 순서를 데코레이트할 수 있다.

// Given
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");

// business logic
Supplier<String> businessFunction = CircuitBreaker
    .decorateSupplier(circuitBreaker, () -> {
        throw new RuntimeException("my test");
    });
// on fail handle logic
Consumer<Throwable> onFailureFunction = throwable -> System.out.println(
    "throwable = " + throwable);
// try
Try<String> result = Try.ofSupplier(businessFunction)
    .onFailure(onFailureFunction)
    .recover(RuntimeException.class, (e) -> "recover hello");

// Then
assertThat(result.isSuccess()).isTrue();
assertThat(result.get()).isEqualTo("recover hello");

다음 코드에서는 fallback처리를 vavr 라이브러리를 이용해서 처리하는 예시이다.

하지만 추천하는 방법은 Decorators 유틸리티를 이용한 방식이다. 좀 더 직관적이고 가독성 있는 코드라고 생각하기 때문이다.

// given
Supplier<String> businessLogic = () -> {
    System.out.println("run business logic");
    throw new RuntimeException();
};

CircuitBreaker circuitBreaker = CircuitBreakerRegistry.ofDefaults()
    .circuitBreaker("test1");

circuitBreaker.getEventPublisher()
    .onError(event -> System.out.println("circuit breaker success"))
    .onSuccess(event -> System.out.println("circuit breaker success"));

Retry retry = RetryRegistry.ofDefaults()
    .retry("retry test");

retry.getEventPublisher()
    .onError(event -> System.out.println("retry fail"))
    .onSuccess(event -> System.out.println("retry success"));
// when
DecorateSupplier<String> finalSupplier = Decorators.ofSupplier(businessLogic)
    .withCircuitBreaker(circuitBreaker)
    .withRetry(retry)
    .withFallback(throwable -> "fall back");
// then
String result = finalSupplier.get();
System.out.println("result = " + result);

더 많은 예시는 다음 공식문서 링크를 참고하라

 

Examples

Examples of resilience4j-circuitbreaker

resilience4j.readme.io

Annotation 기반

이 방식은 좀 더 간결한 코드만으로 구현이 가능하다.

@CircuitBreaker(name = "test", fallbackMethod = "fallback")
public String businessLogic(int p1, String p2){
    return "business";
}

public String fallback(int p1, String p2, Throwable throwable){
    return "fail";
}

주의할 점으로는 스프링에서 AOP를 이용한 프록시 객체를 통해서 circuit breaker를 적용하는데 fallback처리 메서드가 private이고 스프링 컨테이너로부터 주입받은 빈을 사용하는 기능을 fallback에서 사용한다면 NullPointerException이 발생한다는 것이다. 프록시에서는 빈을 주입받지 못하기 때문에 발생하는 오류이기에 fallback 메서드의 접근 권한을 private보다 높게 설정해야 한다.

간단하다는 명확한 장점이 있지만 단점으로는 fallbackMethod의 오타를 컴파일 시점에서 알아채기 힘들다는 점과 테스트 코드를 작성하기 난감하다는 점이 있다. 그래서 무조건 어노테이션 기반의 구현을 사용하기보다는 적절한 경우를 생각하고 사용하는 것이 좋아 보인다.

깃헙 예제 보기

Bulk head

큰 선박에서는 외부의 파손이 생겼을 때, 파손의 영향을 최소화하기 위해서 각 구역을 격리한다. 이때 격벽을 각 구역마다 세워서 분리한다. 소프트웨어에서의 격벽을 적용하는 것이 bulkhead 패턴이다.

언제 사용할까?

해당 패턴은 모놀리식 아키텍처 구조를 가진 애플리케이션에서 적용하면 유용하다고 생각한다. 하나의 서비스의 기능의 장애가 전체 서비스의 자원을 소모하는 상황을 초래한다면 기능적으로 문제가 발생하지 않은 다른 기능도 장애가 발생할 것이다. 이때 벌크 헤드 패턴을 적용해서 서비스를 격리하면 매우 좋을 듯하다. 반면 MSA에서는 이미 서비스를 잘게 쪼개서 실행하기 때문에 하나의 마이크로 서비스의 장애가 다른 서비스로 전파되기는 쉽지 않다. 그래서 MSA를 사용한다면 그 의미가 크지 않을 것으로 보인다.

어떻게 사용할까?

circuit breaker와 매우 동일한 사용법을 가지고 있다. Circuit breaker의 사용법에서 CircuitBreaker를 BulkHead로 변경하면 사용법은 동일하기 때문에 circuit breaker의 사용법을 참고해서 사용하기를 바란다.

사용법은 공식문서를 참고하자!

 

Examples

Examples of resilience4j-bulkhead

resilience4j.readme.io

Retry

어떤 물건이 고장 났을 때, 그냥 다시 한번 해보는 경험이 있지 않은가? 마치 컴퓨터가 고장 나면 재부팅을 하면 그냥 정상적으로 되는 그런 상황?
소프트웨어도 동일하게 그냥 다시 한번 해보았을 때, 정상적으로 동작하는 경우가 있다!

언제 사용할까?

일시적인 장애(순간적인 네트워크 장애 등과 같은 상황)로 인해서 기능이 실패했을 때, 사용하면 유용하다.

어떻게 사용할까?

사용법은 공식문서를 참고하자!

 

Retry

Getting started with resilience4j-retry

resilience4j.readme.io

Time Limiter

시간은 금이다! 너무 오래 걸리는 작업은 그 가치가 무의미한 경우가 존재하기도 한다.

언제 사용할까?

데드락과 같은 특수한 상황에서는 실행을 기다리는 것보다 강제로 종료를 해야 하는 것이 좋다. 그래서 이런 경우에 사용하거나 실행 시간이 중요한 서비스에서 시간을 제한하거나 timeout을 설정하고 싶을 때, 그런 상황에서 사용하면 유용할 듯하다.

어떻게 사용할까?

사용법은 공식문서를 참고하자!

 

TimeLimiter

Getting started with resilience4j-timelimiter

resilience4j.readme.io

 

어노테이션 적용 우선순위

기본 설정에 따르면 다음 그림과 같은 순서로 처리가 된다.

하지만 순서를 커스텀 하고 싶다면 우선순위를 설정함으로써 그 순서를 커스텀하여 사용할 수 있다.

resilience4j.retry.retryAspectOrder
resilience4j.circuitbreaker.circuitBreakerAspectOrder
resilience4j.ratelimiter.rateLimiterAspectOrder
resilience4j.timelimiter.timeLimiterAspectOrder
resilience4j.bulkhead.bulkheadAspectOrder

다음 프로퍼티 속성을 통해서 커스텀할 수 있는 것! 더 높은 값을 가질수록 그 우선순위가 높다. 다음 예제에서는 retrycircuit breaker보다 우선시하여 사용하는 설정한 것이다.

resilience4j:  
  circuitbreaker:  
    circuitBreakerAspectOrder: 1  
  retry:  
    retryAspectOrder: 2

생태계

spring boot, spring cloud, Spring Reactor, Micronaut와 같은 프레임워크와 연동하여 사용할 수 있다.
모니터링 도구들과 연동하는 것도 제공하여 Micrometer, Grafana에서 보다 쉽게 매트릭 정보를 모니터링할 수 있다.
spring boot와 spring cloud와 연동해서 사용한다면 spring actuator를 사용해서 spring 애플리케이션의 추가적인 매트릭 정보와 연동되도록 쉽게 구성할 수 있다는 점이 매력적으로 보였다.

마치며

어플리케이션 단에서 외부의 장애에 대처하는 로직을 쉽게 작성할 수 있다는 점이 매력적인 라이브러리이다. 처음 사용할 때는 익숙하지 않고 어떻게 사용하면 좀 더 깔끔하게 분리해서 사용할 수 있을까? 손쉬운 테스트를 작성하기 쉬운 구조는 어떤 것일까에 대해서 고민이 많이 되어서 쉽게 적용하기 어려웠지만 한번 사용하고 나니 그런 고민들이 정리가 되어서 추후에는 빠르게 적용할 수 있을 것 같다.

공식 문서도 정리가 깔끔했고 관련된 코드 샘플도 제공을 해주어서 학습하기에는 아주 어려운 라이브러리는 아닌 것 같으니 장애에 대응하는 애플리케이션을 개발하는 필요가 있다면 한번 적용해 보는 것은 어떨까?

Reference

 

resilience4j

 

resilience4j.readme.io