Difference Between Circuit Breaker and Retry in Spring Boot

1. 개요

분산 시스템 및 마이크로서비스 아키텍처에서 고장을 원활하게 처리하는 것은 시스템의 신뢰성과 성능을 유지하는 데 매우 중요합니다. 이를 달성하는 데 도움을 주는 두 가지 기본적인 복원력 패턴은 서킷 브레이커(Circuit Breaker)와 재시도(Retry)입니다. 두 패턴 모두 시스템의 안정성과 신뢰성을 개선하는 것을 목표로 하지만, 각각의 용도와 적용되는 시나리오는 명확히 다릅니다.

이 글에서는 Resilience4j를 사용한 Spring Boot의 메커니즘, 사용 사례 및 구현 세부 사항을 포함하여 이러한 패턴을 심도 있게 탐구합니다.

2. 재시도(Retry)란 무엇인가?

재시도 패턴은 분산 시스템에서 일시적인 실패를 처리하는 간단하면서도 강력한 메커니즘입니다. 작업이 실패할 경우, 재시도 패턴은 같은 작업을 여러 번 실행하여 일시적인 문제가 해결되기를 희망합니다.

2.1. 재시도의 주요 특징

재시도 메커니즘은 일시적인 문제를 효과적으로 처리하는 데 도움이 되는 특정 특성을 중심으로 구성되어, 일시적인 오류가 심각한 문제로 발전하지 않도록 합니다:

  • 반복 시도: 핵심 아이디어는 실패한 작업을 지정된 횟수만큼 다시 실행하는 것입니다.
  • 백오프 전략: 이는 시스템을 과부하에서 보호하는 데 도움을 주는 지수 백오프와 같은 고급 재시도 메커니즘입니다.
  • 일시적인 실패에 적합: 간헐적인 네트워크 문제, 일시적인 서비스 사용 불가, 또는 순간적인 자원 제약에 가장 적합합니다.

2.2. 재시도 구현 예제

Resilience4j를 사용하여 재시도 메커니즘을 구현하는 간단한 예제를 살펴보겠습니다:

@Test
public void whenRetryWithExponentialBackoffIsUsed_thenItRetriesAndSucceeds() {
    IntervalFunction intervalFn = IntervalFunction.ofExponentialBackoff(1000, 2);
    RetryConfig retryConfig = RetryConfig.custom()
        .maxAttempts(5)
        .intervalFunction(intervalFn)
        .build();

    Retry retry = Retry.of("paymentRetry", retryConfig);

    when(paymentService.process(1)).thenThrow(new RuntimeException("First Failure"))
          .thenThrow(new RuntimeException("Second Failure"))
          .thenReturn("Success");

    Callable<String> decoratedCallable = Retry.decorateCallable(
      retry, () -> paymentService.processPayment(1)
    );

    try {
        String result = decoratedCallable.call();
        assertEquals("Success", result);
    } catch (Exception ignored) {
    }

    verify(paymentService, times(3)).processPayment(1);
}

이 예제에서:

  • 재시도 메커니즘은 작업을 최대 5회 시도합니다.
  • 지수 백오프 전략을 사용하여 시도 간 대기 시간을 도입하여 시스템 부하를 줄입니다.
  • 작업은 두 번의 재시도 후에 성공합니다.

3. 서킷 브레이커 패턴이란 무엇인가?

서킷 브레이커 패턴은 고장을 처리하는 보다 고급 접근법입니다. 실패할 가능성이 있는 작업을 반복적으로 실행하지 않도록 애플리케이션을 방지하여, 누수 실패를 막고 시스템 안정성을 제공합니다.

3.1. 서킷 브레이커의 주요 특징

서킷 브레이커 패턴은 실패하는 서비스에 대한 과도한 부하를 방지하고 누수 실패를 완화하는 데 초점을 맞춥니다. 주요 속성은 다음과 같습니다:

  • 상태 관리: 서킷 브레이커는 세 가지 기본 상태가 있습니다:

    • 닫힘(Closed): 정상 작동, 요청을 계속 허용
    • 열림(Open): 모든 요청을 차단하여 추가 실패 방지
    • 반쯤 열림(Half-Open): 시스템이 복구되었는지 확인하기 위해 제한된 수의 테스트 요청을 허용
  • 실패 임계값: 슬라이딩 윈도우 내에서 실패한 요청의 비율을 모니터링하고, 실패율이 설정된 임계값을 초과하면 서킷이 “트립”됩니다.

  • 누수 실패 방지: 실패하는 서비스에 대한 반복 호출을 중단하여 전체 시스템의 저하를 방지합니다.

3.2. 서킷 브레이커 구현 예제

상태 전환을 보여주는 간단한 서킷 브레이커 구현 예제는 다음과 같습니다:

@Test
public void whenCircuitBreakerTransitionsThroughStates_thenBehaviorIsVerified() {
    CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
        .failureRateThreshold(50)
        .slidingWindowSize(5)
        .permittedNumberOfCallsInHalfOpenState(3)
        .build();

    CircuitBreaker circuitBreaker = CircuitBreaker.of("paymentCircuitBreaker", circuitBreakerConfig);

    AtomicInteger callCount = new AtomicInteger(0);

    when(paymentService.processPayment(anyInt())).thenAnswer(invocationOnMock -> {
        callCount.incrementAndGet();
        throw new RuntimeException("Service Failure");
    });

    Callable<String> decoratedCallable = CircuitBreaker.decorateCallable(
      circuitBreaker, () -> paymentService.processPayment(1)
    );

    for (int i = 0; i < 10; i++) {
        try {
            decoratedCallable.call();
        } catch (Exception ignored) {
        }
    }

    assertEquals(5, callCount.get());
    assertEquals(CircuitBreaker.State.OPEN, circuitBreaker.getState());

    callCount.set(0);
    circuitBreaker.transitionToHalfOpenState();

    assertEquals(CircuitBreaker.State.HALF_OPEN, circuitBreaker.getState());
    reset(paymentService);
    when(paymentService.processPayment(anyInt())).thenAnswer(invocationOnMock -> {
        callCount.incrementAndGet();
        return "Success";
    });

    for (int i = 0; i < 3; i++) {
        try {
            decoratedCallable.call();
        } catch (Exception ignored) {
        }
    }

    assertEquals(3, callCount.get());
    assertEquals(CircuitBreaker.State.CLOSED, circuitBreaker.getState());
}

이 예제에서:

  • 50%의 실패율 임계값과 다섯 번의 호출로 이루어진 슬라이딩 윈도우가 서킷 브레이커가 “트립”되는 기준을 결정합니다.
  • 다섯 번의 실패 시도가 있으면 서킷이 열리면서 즉시 추가 호출을 거부합니다.
  • 1초 대기 후 서킷이 반쯤 열림 상태로 전환됩니다.
  • 반쯤 열림 상태에서 세 번의 성공적인 호출이 이루어져 서킷 브레이커가 닫힘 상태로 전환되고 정상 작동을 재개합니다.

4. 재시도와 서킷 브레이커의 주요 차이점

측면 재시도 패턴 서킷 브레이커 패턴
기본 목표 여러 번 작업 시도 실패하는 서비스에 대한 반복 호출 방지
실패 처리 일시적인 실패 가정 잠재적인 시스템 실패 가정
상태 관리 상태 없음, 반복 시도 상태 유지 (닫힘/열림/반쯤 열림)
가장 잘 사용되는 경우 간헐적이고 복구 가능한 오류 지속적이거나 시스템적인 실패

5. 각 패턴을 사용할 시기

재시도 또는 서킷 브레이커 사용 시기를 결정하는 것은 시스템이 겪고 있는 실패의 유형에 달려 있습니다. 이 패턴들은 서로 보완적이며, 그 적용을 이해하는 것은 오류를 효과적으로 처리하는 복원력 있는 시스템 구축에 도움이 됩니다.

  • 재시도를 사용해야 할 때:

    • 일시적인 네트워크 문제를 처리할 때
    • 일시적인 서비스 사용 불가가 예상될 때
    • 몇 번의 재시도로 빠른 복구가 가능할 때
  • 서킷 브레이커를 사용해야 할 때:

    • 장기적인 서비스 실패로부터 보호할 때
    • 마이크로서비스에서 누수 실패를 방지할 때
    • 자가 치유 시스템 아키텍처를 구현할 때

실제 애플리케이션에서는 이 패턴들이 함께 사용되는 경우가 많습니다. 예를 들어, 재시도 메커니즘이 서킷 브레이커 내에서 작동하여 서킷이 닫혀 있거나 반쯤 열림 상태일 때만 재시도가 시도됩니다.

6. 모범 사례

이러한 패턴의 효과를 극대화하기 위해:

  • 메트릭 모니터링: 실패율, 재시도 시도 및 서킷 상태를 지속적으로 모니터링하여 구성을 세밀하게 조정합니다.
  • 패턴 결합: 일시적인 오류에는 재시도를, 시스템적 실패에는 서킷 브레이커를 사용합니다.
  • 현실적인 임계값 설정: 지나치게 공격적인 임계값은 복구를 방해하거나 실패 감지를 지연시킬 수 있습니다.
  • 라이브러리 활용: Resilience4j와 같은 강력한 라이브러리 또는 Spring Cloud Circuit Breaker를 사용하여 구현을 단순화합니다.

7. 스프링 부트 통합

스프링 부트는 생태계를 통해 서킷 브레이커와 재시도 패턴 모두에 대한 포괄적인 지원을 제공합니다. 이 통합은 주로 스프링 클라우드 서킷 브레이커 프로젝트와 스프링 재시도 모듈을 통해 이루어집니다.

스프링 클라우드 서킷 브레이커 프로젝트는 특정 구현에 묶이지 않고 서킷 브레이커를 구현할 수 있도록 하는 추상화 계층을 제공합니다. 이는 우리의 필요에 따라 Resilience4j, Hysterix, Sentinel 또는 Spring Retry와 같은 서로 다른 서킷 브레이커 구현 간에 변경할 수 있음을 의미합니다. 이 프로젝트는 스프링 부트의 자동 구성 메커니즘을 사용하여, 클래스패스에 적절한 스타터를 감지하면 필요한 서킷 브레이커 빈을 자동으로 구성합니다.

재시도 기능에 대해 스프링 부트는 스프링 재시도와 통합되어 재시도 로직 구현에 대한 주석 기반 및 프로그래밍 방식 접근을 제공합니다. 이 프레임워크는 재시도 시도, 백오프 정책 및 복구 전략을 사용자 지정할 수 있는 유연한 구성 옵션을 제공합니다.

이러한 패턴의 스프링 부트 통합의 몇 가지 특징은 다음과 같습니다:

  • 자동 구성 지원: 스프링 부트는 클래스패스의 종속성에 따라 서킷 브레이커 및 재시도 빈을 자동으로 구성하여 보일러플레이트 구성 코드를 줄입니다.
  • 플러그 가능 아키텍처: 추상화 계층을 통해 비즈니스 논리를 수정하지 않고도 서로 다른 서킷 브레이커 구현 간에 전환할 수 있습니다.
  • 구성 유연성: 두 패턴 모두 애플리케이션 프로퍼티 또는 자바 구성 통해 구성할 수 있으며, 서로 다른 서비스에 대해 전역 및 특정 구성을 지원합니다.
  • 스프링 생태계와의 통합: 이러한 패턴은 RestTemplate, WebClient, 및 다양한 스프링 클라우드 구성 요소와 원활하게 작동합니다.
  • 모니터링 및 메트릭: 스프링 부트의 액추에이터 통합은 서킷 브레이커 및 재시도 시도를 위한 내장 모니터링 기능을 제공하여 복원력 메커니즘의 건강과 동작을 추적하는 데 도움을 줍니다.

이러한 통합 접근법은 스프링 부트의 구성보다 관습 철학에 부합하며, 필요한 경우 동작을 사용자 지정할 수 있는 유연성을 유지합니다. 이 프레임워크의 이러한 패턴 지원으로 인해 실패를 원활하게 처리하고 시스템의 안정성을 유지하는 복원력 있는 마이크로서비스를 더 쉽게 구축할 수 있습니다.

8. 결론

재시도와 서킷 브레이커는 분산 시스템에서 필수적인 복원력 패턴입니다. 재시도가 즉각적인 복구에 초점을 맞춘 반면, 서킷 브레이커는 누수 실패에 대한 강력한 보호를 제공합니다. 그들의 차이점과 사용 사례를 이해함으로써 우리는 신뢰할 수 있고 장애 허용적인 시스템을 설계할 수 있습니다.

Resilience4j 및 스프링 클라우드 서킷 브레이커와 같은 라이브러리를 통해 스프링 부트는 이러한 패턴을 손쉽게 구현할 수 있는 강력한 플랫폼을 제공합니다. 이러한 복원력 전략을 채택함으로써 우리는 불리한 조건에서도 원활한 사용자 경험을 보장하는 애플리케이션을 구축할 수 있습니다.

일반적으로 이 기사에서 제시된 모든 예제를 포함한 전체 코드는 GitHub에서 확인할 수 있습니다.

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다