본문 바로가기
개발 활동/스위프

외부 API 연동 - 외부 API에 문제가 있으면? Circuit Breaker Pattern & Resilience4j

by 다음에바꿔야지 2024. 5. 30.

ChatGPT API를 이용하면서 외부 API에 여러가지 에러가 발생할때 대처법에 대해 공부해보았다.

API를 통해 AI에게 요청을 날렸으나 응답을 받지 못하면 Timeout이 발생할때까지 요청 스레드를 계속 점유하고 있을 것이고, 응답이 지연되는 경우에는 Latency 시간만큼 요청 스레드를 점유하고 있을 것이다.

이렇게 계속 스레드를 점유하고 있다면 서버의 스레드풀을 갉아먹어서 서비스 전체의 장애로 이어질 수 있다.

 

그렇다면, '이미 장애가 발생했을때는 더 이상 요청을 보내지 않아 자원을 사용하지 않을 수 없을까?' 라는 생각이 들었다.

이를 해결하기 위해 Circuit Breaker 패턴을 적용하기로 했다.

 

1. Circuit Breaker Pattern?

: 장애를 방지하기 위한 패턴으로 장애 발생 지점을 감지하고 실패하는 요청을 계속적으로 보내지 않도록 방지하는 패턴.

'회로 차단기'라는 의미이다.

이를 서비스에 대입해보면, 각 서비스들의 상황을 모니터링해 감지하고, 하나의 서비스에 장애가 발생하면 요청을 차단(Switch Open)하여 해당 서비스로의 요청이 빠르게 실패하도록 하는 것이다.

 

Circuit Breaker은 3가지 상태가 존재한다. 상태를 결정하는 기준은 '실패 임계치'이다.

  1. Closed
    : 실패율이 실패 임계치보다 낮은 상태. 서비스 정상 작동 상태.
  2. Open
    : 실패율이 실패 임계치보다 높은 상태. 서비스로 요청을 보내지 않고 즉시 실패 처리됨.
  3. Half-Open
    : Open 상태 이후 일정 시간이 지난 상태. 이후 요청 성공/실패에 따라 Closed/Open으로 바뀐다.

+ 실패 임계치는 slow call(지연 요청), failure call(실패 요청)에 따라 값이 정해진다.

 

2. Circuit Breaker 살펴보기

서킷 브레이커 패턴을 적용하기 위해 방법을 찾아보았다. 라이브러리는 2가지가 존재했다.

  1. Netflix Hystrix
  2. Resilience4j

Hystrix는 더이상 개발이 되지 않고, 유지보수 모드임을 알게 되었고, 새로운 프로젝트에는 Resilience4j를 권장하는 공식문서를 보았다.

Resilience4j로 구현해보자!

 

먼저 Resilience4j에서는 어떻게 Circut Breaker Pattern을 구현하는지 알아보자.

 

2-1. 슬라이딩 윈도우

각 호출 결과를 저장하고 집계하는데 '슬라이딩 윈도우'를 사용한다.

  • Count-based sliding window : 요청 개수 단위로 요청을 저장 및 집계하는 슬라이딩 윈도우
  • Time-based sliding window : 요청 시간 단위로 요청을 저장 및 집계하는 슬라이딩 윈도우

 

2-2. Fallback

: 서비스를 차단한 경우 예외을 발생시키는 대신 미리 준비된 동작을 실행하는 것을 말한다.

이 기능은 Open 상태인 경우 사용자 요청을 에러로 응답하지 않고 성공으로 응답하도록 작동한다.

외부 서비스에서 장애가 발생했을 때, 적절한 응답을 받지 못해 우리 서비스에서도 에러가 발생해 사용자는 에러 화면을 보게 될 것이다. 이는 곧 사용자 경험의 부정적인 요소로 다가간다. 이런 상황을 막기 위해 Fallback을 사용해 외부 서비스에서는 장애가 발생했지만, 사용자에게는 미리 설정한 응답값을 보내주어 장애가 발생하지 않은 것처럼 보이게 할 수 있다.

 

2-3. Bulkhead

: 응답시 지연되는 서비스에 자원을 모두 소진하지 않도록 스레드 풀을 격리하는 것을 의미한다.

MSA 구조에서 여러 서비스가 외부 API를 호출할때 계속 지연이 발생하면 장애가 한 곳 뿐만 아니라 여러 곳으로 전파될 수 있다. 이러한 장애 전파를 막기 위해 각 서비스에서 외부 API를 호출할 때 스레드 격리 환경을 설정할 수 있다.

 

 

3. Resilience4j 적용

우리 프로젝트에 Resilience4j를 적용해보겠다.

 

<build.gradle>

Resilience4j 의존성 추가

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'

 

<application.properties>

resilience4j.circuitbreaker.instances.customCircuitBreaker.registerHealthIndicator=true
resilience4j.circuitbreaker.instances.customCircuitBreaker.slidingWindowSize=10
resilience4j.circuitbreaker.instances.customCircuitBreaker.failureRateThreshold=50

resilience4j.retry.instances.myRetry.maxAttempts=3
resilience4j.retry.instances.myRetry.waitDuration=1s

 

  • registerHealthIndicator=true
    • Health Indicator를 등록할지 여부를 설정. Health Indicator는 Circuit Breaker의 상태를 모니터링할 수 있도록 해주며, Spring Boot Actuator와 함께 사용하면 /actuator/health 엔드포인트를 통해 Circuit Breaker의 상태를 확인할 수 있다.
  • slidingWindowSize=10
    • 슬라이딩 윈도우의 크기를 설정. Circuit Breaker가 최근의 호출 결과를 얼마나 많이 저장할지를 정의. 최근 10번의 호출 결과를 저장하도록 설정.
  • failureRateThreshold=50
    • 실패율 임계값을 설정. Circuit Breaker는 최근 호출 결과를 분석하여 실패율이 이 값 이상이 되면 열리게 된다. 실패율이 50%를 넘으면 Circuit Breaker가 열리도록 설정.

 

 

  • maxAttempts=3
    • 최대 재시도 횟수를 설정. 실패한 호출을 최대 몇 번까지 재시도할지를 정의. 최대 3번까지 재시도하도록 설정.
  • waitDuration=1s
    • 재시도 사이의 대기 시간 설정. 재시도 사이에 1초씩 대기하도록 설정.

 

다른 설정값이 궁금하다면 공식문서를 참고하면 된다. => https://resilience4j.readme.io/docs/circuitbreaker

 

<OpenAiClient>

OpenFeign 인터페이스에서 @CircuitBreaker를 선언해 서킷 브레이커 설정

@FeignClient(name = "OpenAiClient", url = "${openai.url}")
public interface OpenAiClient {
    @PostMapping(value = "/v1/chat/completions", consumes = APPLICATION_JSON_VALUE)
    @CircuitBreaker(name = "openAiClient", fallbackMethod = "fallbackGetAiComment")
    @Retry(name = "openAiClient")
    CommentResponse getAiComment(@RequestHeader("Authorization") String authorization, @RequestBody CommentRequest commentRequest);
}

getAiComment()가 호출될때 'openAiClient'라는 서킷 브레이커 인스턴스가 실행되도록 했다.

 

<AiServiceImpl>

    @CircuitBreaker(name = "openAiClient", fallbackMethod = "fallbackGetAiComment")
    @Retry(name = "openAiClient")
    private String getAiComment(CommentRequest commentRequest) {
        CommentResponse response = openAiClient.getAiComment(apiKey, commentRequest);
        log.debug("API Response: {}", response);

        String aiComment = response
                .getChoices()
                .stream()
                .findFirst()
                .map(choice -> choice.getMessage().getContent())
                .orElseThrow(() -> new EmptyCommentException("fail to get ai comment"));
        log.debug("AI 한마디를 성공적으로 요청했습니다.");
        return aiComment;
    }

    private String fallbackGetAiComment(Throwable t) {
        log.error("Fallback method for getAiComment due to : {}", t.getMessage(), t);
        throw new BusinessException(ErrorCode.AI_NETWORK.name(), ErrorCode.AI_NETWORK);
    }

fallbackMethod를 설정해 커스텀 에러를 발생시켜 클라이언트에서 처리할 수 있도록 했다.

 

이러한 패턴을 적용하는 것은 외부 API의 장애가 우리 서비스의 장애로 연결되지 않도록 하여 사용자의 경험을 덜 하락시킬 수 있는 것 같다.

Resilience4j의 설정값, Bulkhead 등 관련하여 더 스터디가 필요할 것 같지만 성공적으로 Circuit Breaker Pattern을 적용할 수 있었다.

 

다음 글은 외부 API를 Mock 서버를 이용해 테스트하는 방법에 대해 알아볼 것이다.(WireMock)

 

 

참고 자료:

https://ksh-coding.tistory.com/142