코딩은 내일부터

Resilience4j를 사용해서 장애 내성 기르기 본문

우아한 테크 코스(우테코)/우테코 공부

Resilience4j를 사용해서 장애 내성 기르기

zl존 비버 2023. 11. 8. 17:05
728x90

프로젝트를 진행하면서 외부 API를 다루는 경우가 많았다.

 

2023.09.14 - [우아한 테크 코스(우테코)/우테코 공부] - 파사드 패턴으로 외부 API와 DB 트랜잭션 범위 분리하기

 

파사드 패턴으로 외부 API와 DB 트랜잭션 범위 분리하기

DB 커넥션은 개수가 제한적이므로 단위 프로그램이 커넥션을 소유하는 시간이 길어질수록 사용 가능한 커넥션의 개수는 줄어든다. 따라서 하나의 작업이(트랜잭션이 필요 없는 로직이 붙어있는

jeoninpyo726.tistory.com

위와 같이 외부 API와 비즈니스 로직을 분리하였는데 외부 API가 장애가 났을 때 전체 로직이 피해받는 부분은 똑같았다.

 

그래서 Resilience4j라는 장애에 강한 시스템을 만드는데 도와주는 라이브러리를 공부해보았다.

 

 

Resilience4j란?

먼저 Resilience라는 단어의 뜻은 탄력성이라는 뜻을 갖고 있다.

Resilience4j라는 이름을 다시 보면 장애에 대한 회복탄력성을 가지게 해주는 라이브러리라고 다시 해석할 수 있다.

 

Resilience4j와 비슷한 Hystrix라는 오픈소스도 있지만,

Hystrix는 2018년 11월까지 개발이 되었고, 그 이후부터 지금까지는 유지보수만 이루어지고 있다.

Hystrix저장소에도 Hystrix대신 Resilience4j를 사용하라고 권장하고 있다.

그래서 Resilience4j를 사용하는 게 더 적합하다고 생각해서 Resilience4j를 알아보도록 하겠다.

 

 

 

기능

 

Resilience4j는 위와 같은 6개의 모듈이 있는데 간단히 말해서 6개의 기능이라고 생각하면 된다.

6개의 기능들 중 이번 포스팅에서는 일부기능만 알아보겠다.

 

시작하기 전 의존성 주입하기

Resilience4j를 사용하기 위해서는 다음과 같이 의존성을 주입해줘야 한다.

첫 번째와 두 번째 설정은 웹 프로젝트 기본설정이 거 세 번째 설정의 현재 프로젝트에 Spring Boot의 버전과 맞게 설정하면 된다.

마지막으로 Spring Boot Starter AOP까지 설정해 주면 끝이다.

 

 

Retry

먼저 Retry기능을 알아보면 단어 그대로 다시 요청을 하는 기능이다.

  • 몇 번까지 재시도할 것인가?
  • 재시도 간격은 얼마나 길게 줄 것인가?
  • 어떤 상황을 호출 실패로 간주할 것인가?

재시도를 할 때 위의 3가지 사항을 고려할 수 있는데 마지막에 고려할 상황만 다시 살펴보면

만약 Null에러와 같은 잘못된 요청 혹은 개발자의 잘못된 구현으로 발생할 수 있는 에러들은 재시도를 한다고 회복되는 상황이 아닐 거다.

따라서 네트워크 통신과 같은 에러를 고려해봐야 한다.

 

구현 코드

application.yml

resilience4j.retry:
  configs:
    default:
      maxAttempts: 3
      waitDuration: 1000
      retryExceptions:
        - com.example.resilience4jdemo.exception.RetryException
      ignoreExceptions:
        - com.example.resilience4jdemo.exception.IgnoreException
  instances:
    beaverRetryConfig:
      baseConfig: default

resilience4j.retry의 instances는 resilience4j의 재시도 기능을 인스턴스화하고,

이 인스턴스의 이름을 beaverRetryConfig로 지정하겠다는 뜻이다.

 

기본 설정(baseConfig)은 default로 지정되어 있는데, 이는 configs에서 default라는 이름으로 정의된 설정을 사용하겠다는 의미다. default 설정을 살펴보면, 최대 3번(maxAttempts)까지 재시도하며, 재시도 사이에 1초(waitDuration)의 대기 시간을 가지고

특정 예외(retryExceptions)가 발생할 경우 재시도하도록 설정되어 있으며,

여기서는 com.example.resilience4 jdemo.exception.RetryException이 해당된다.

반면, ignoreExceptions에 명시된 예외는 재시도하지 않고 무시하도록 하는 설정이다.

 

Service 로직

@Service
public class RetryService {

    private static final String BEAVER_RETRY_CONFIG = "beaverRetryConfig";

    @Retry(name = BEAVER_RETRY_CONFIG, fallbackMethod = "fallback")
    public String process(String param) {
        return callAnotherServer(param);
    }

    private String fallback(String param, Exception ex) {
        return "Recovered: " + ex.toString();
    }

    private String callAnotherServer(String param) {
        throw new RetryException("retry exception");
    }
};

 

@Retry어노테이션의 name은. yml에서 사용하고 싶은 instance 이름 작성한다.

fallbackMethod는 설정한 retry의 모두 실패했을 때 해당 이름을 가지는 메서드를 실행시킨다는 의미다.

 

@Retry어노테이션만 사용하면 재시도할 수 있다는 이점이 있지만

개인적인 생각으로는 Retry기능을 쓰기 위해 Resilience4j라이브러리 사용은 불필요한 거 같다.

 

 

CircuitBreaker

API요청을 보낼 때 서버에 많은 부하를 받고 있을 때 Retry를 한다고 회복한다고 기대할 수 없다.

트래픽이 내려가기를 기다려야 하는데 이러한 상황에서 트래픽을 차단시켜줄 때 circuitBreaker를 사용할 수 있다.

추가적으로 circuitBreaker는  회로차단이라는 뜻을 가지고 있다.

 

예를 들면

활활

상품목록 조회 API는 상품과 리뷰저장소에서 정보를 가져와 요청된 데이터를 응답한다고 하면

만약 리뷰저장소에서 부하가 많아 응답이 느리다고 했을 때 상품목록 조회 기능 자체가 느려질 것이다.

그러면 해당 기능을 요청한 사용자는 기대하는 페이지가 안 보여 계속 새로고침을 누르면 해당 기능은 회복되지 않을 수 있다.

 

이럴 때 CircuitBreaker기능을 사용해서 회복될 때까지 해당 리뷰 저장소 API요청을 차단시킬 수 있다.

 

 

CircuitBreaker의 사이클은 위의 사진처럼 흘러간다.

  1. 에러가 설정해 놓은 임계값을 넘어가면 OPEN으로 상태를 바꾼다.
  2. 설정한 시간이 지나면 HALF_OPEN으로 상태를 바꾼다.
  3. HALF_OPEN에서 시스템이 회복됐는지 점검한다.
    1. 설정한 횟수만큼 요청이 정상적으로 실행되면 CLOSE로 상태를 변경한다.
    2. 설정한 횟수만큼 요청이 정상적으로 실행되지 않았다면 OPEN으로 상태를 변경한다.

구현 코드

application.yml

resilience4j.circuitbreaker:
  configs:
    default:
      slidingWindowType: COUNT_BASED
      minimumNumberOfCalls: 7
      slidingWindowSize: 10
      waitDurationInOpenState: 10s

      failureRateThreshold: 40

      slowCallDurationThreshold: 4000
      slowCallRateThreshold: 60

      permittedNumberOfCallsInHalfOpenState: 5
      automaticTransitionFromOpenToHalfOpenEnabled: false

      eventConsumerBufferSize: 10

      recordExceptions:
        - com.example.resilience4jdemo.exception.RecordException
      ignoreExceptions:
        - com.example.resilience4jdemo.exception.IgnoreException
  instances:
    beaverCircuitBreakerConfig:
      baseConfig: default

 

해당 설정은 공식문서를 참고하면 된다.

 

 

Service 로직

@Service
public class CircuitBreakerService {

    private static final String BEAVER_CIRCUIT_BREAKER_CONFIG = "beaverCircuitBreakerConfig";

    @CircuitBreaker(name = BEAVER_CIRCUIT_BREAKER_CONFIG, fallbackMethod = "fallback")
    public String process(String param) throws InterruptedException {
        return callAnotherServer(param);
    }

    private String fallback(String param, Exception ex) {
        return "Recovered: " + ex.toString();
    }

    private String callAnotherServer(String param) throws InterruptedException {
        if ("a".equals(param))
            throw new RecordException("record exception");
        else if ("b".equals(param))
            throw new IgnoreException("ignore exception");
        else if ("c".equals(param))
            Thread.sleep(4000);
        return param;
    }
};

@CircuitBreaker어노테이션 또한 @Retry와 동일하다.

. yml에서 설정한 인스턴스를 사용한다고 명시해 주고 OPEN으로 상태가 바뀌었을 때 fallback해주는 메서드 이름을 넣으면 끝이다.

한 가지 주의할 점은 OPEN인 상태에서 설정한 RecordException 뿐만 아니라 다른 예외도 fallback메서드가 실행된다.

따라서 해당 fallback메서드의 파라미터를 Exception이 아닌 RecordException으로 변경해 주면

RecordException이 발생할 때만 fallback메서드가 실행된다.

 

 

마치며

Resilience4j를 공부해 봤는데 Resilience4j 또한 AOP로 작동을 해서 Self Invocation, 낮은 응집도 등등

여러 문제점들이 있지만 외부 API가 장애가 났을 때 해결할 수 있는 방법 중 하나로 사용할 수 있을 거 같다.