Medium

Retry 와 Fallback 메카니즘을 활용한 스프링 마이크로서비스 회복탄력성

ppthejake 2023. 10. 26. 12:25

해당 포스트는 Meduim 의 아티클 "Spring Microservices Resilience with Retry and Fallback Mechanisms" 을 번역한 내용이다.

[ 원문 ]

개요

 마이크로서비스 아키텍처 스타일과 같은 분산 시스템 환경에서 서비스 복원력을 보장하는 것은 중요합니다. Spring F/W을 기반으로 구축된 Spring Cloud는 탄력적인 마이크로서비스를 구축하는데 도움이 되는 여러 도구와 기능을 제공합니다. 그 중에서도 Retry 와 Fallback 메커니즘은 실패를 우아하게 처리할 수 있는 강력한 시스템을 구현하는 중요한 구성 요소입니다. 이 글에서는 이러한 메커니즘을 스프링 마이크로서비스와 함께 효과적으로 사용할 수 있는 방법에 대해 소개합니다.

마이크로서비스에서 복원력이 필요한 이유

분산 환경에서 마이크로서비스는 민첩성, 확장성, 유지보수성을 높이는 데 핵심적인 역할을 합니다. 이러한 장점으로 마이크로서비스는 널리 사용되는 아키텍처이지만, 시스템 안정성을 위협할 수 있는 문제도 발생합니다. 따라서 이러한 환경에서 회복탄력성을 확보하는 것은 타협할 수 없는 과제이며, 회복탄력성이 왜 필수적인지 이해하면 강력한 시스템을 구축할 수 있는 토대를 마련할 수 있습니다.

분산환경

마이크로서비스의 핵심에는 분산의 원칙이 내재되어 있습니다. 개별 서비스는 독립적으로 유지,확장 및 배포될 수 있지만, 일관되게 동작하기 위해서는 네크워크 통신에 크게 의존합니다.

  • 예측 불가능한 네트워크 : 네트워크 인프라는 예측할 수 없는 상황으로 가득합니다. 지연 증가, 패킷 손실 및 중단은 드문 일이 아닙니다. 회복탄력성이 있는 마이크로서비스는 네트워크 문제가 이론적 이상이 아니라 항상 발생할 수 있는 상황임을 고려해야 설계되어야 합니다.
  • 서비스 간 통신 : 모노리식 아키텍처에서는 모듈이 메모리 내에서 통신합니다. 그러나 마이크로서비스는 네트워크를 통해 통신하므로 잠재적인 장애 포인트가 존재합니다. HTTP 요청, 메시지 대기열 또는 이벤트 주도 아키텍처는 모두 분산 통신의 문제점을 가지고 있습니다.

연쇄 오류

상호 연결된 환경에서는 하나의 서비스가 실패하면 연쇄 반응을 일으킬 수 있습니다.

  • 종속성 체인 : 서비스A 는 서비스B에 의존하고 서비스B는 서비스 C에 의존 할 수 있습니다. 서비스C가 실패하고 이 실패가 올바르게 처리되지 않으면 서비스A와 B 모두 간접적으로 영향을 받아 시스템 전체의 붕괴로 이어질 수 있습니다.
  • 도미노 효과 방지 : 내구성 있는 설계는 이러한 '도미노 효과'를 방지합니다. 서킷브레이크와 같은 기술은 연쇄오류를 막을 수 있습니다.

외부 종속성

마이크로서비스는 섬이 아니라, 외부 시스템, 서드파티 서비스, 데이터베이스 등과 상호 작용을 합니다.

  • 서드파티 서비스 장애 : 만약 마이크로서비스가 서드파티 서비스에 의존하고 해당 서비스가 다운되면, 그로 인하여 우리의 서비스도 다운될 수 있습니다. 회복탄력성을 고려한 설계는 이러한 잠재적 상황을 대비하는 것을 의미하며, 서드파티 데이터 캐싱이나 대체 메커니즘을 사용하여 대응할 수 있습니다.
  • 데이터베이스 문제 : 데이터베이스는 안정적이지만 오류가 없는것은 아닙니다. 커넥션 풀 부족, 슬로우 쿼리, 데이터베이스 다운과 같은 상황은 마이크로서비스를 작동 불능을 만들 수 있습니다. 데이터베이스 장애 조치 메카니즘, 읽기전용 레플리카, 쿼리 최적화와 같은 전략은 반드시 필요합니다.

확장성과 부하 (Scalability and Load)

마이크로서비스의 주요 장점 중 하나는 수요에 따라 개별 서비스를 확장할 수 있는 것입니다. 그러나 이러한 확장에도 어려움이 따릅니다.

  • 갑작스런 트래픽 증가 : 트래픽의 갑작스러운 증가는 적절하게 처리되지 않을 경우 서비스 다운으로 이어질 수 있습니다. 로드밸런서, 오토 스케일링 정책, 레이트 리미트 등을 통하여 우아하게 대응해야 합니다.
  • 리소스 관리 : 서비스가 확장되면 리소스 관리가 중요해집니다. 단일 서비스 인스턴스에서의 메모리 누수 또는 비효율적인 리소스 사용은 여러 인스턴스에 걸쳐 증폭되어 시스템 전체에 문제를 일으킬 수 있습니다.

마이크로서비스 패러다임에 내재된 이러한 과제를 이해함으로써 개발자와 아키텍터는 기능뿐만 아니라 회복탄력성을 가진 신뢰성 있는 시스템을 설계할 수 있습니다.

Spring Retry

분산 시스템에서 짧은 네트워크 중단이나 일시적인 서비스 중지와 같은 일시적인 장애는 흔한 일입니다. 일부 오류는 지속적이어서 수동 처리나 시스템 변경이 필요할 수 있지만, 대부분은 일시적이므로 다시 시도함으로써 해결 될 수 있습니다. 이때 Spring Retry 를 사용할 수 있습니다. 

개요

Spring Retry 는 재시도 작업에 대한 추상화를 제공하여 어플리케이션에 로직을 손쉽게 추가할 수 있습니다. 이는 일시적인 오류가 발생할 수 있는 원격 서비스나 외부 시스템을 처리할 때 유용합니다.

기본 사용법  

Spring Retry를 사용하려면 적절한 디펜던시를 추가하고 실패 시 재시도를 위한 어노테이션을 추가해야 합니다.

@Service
public class MyService {

    @Retryable(value = Exception.class, maxAttempts = 3)
    public String someOperation() {
        // ... logic that might fail
    }
}

위의 코드에서 @Retryable 어노테이션은 someOperation메소드에서 예외가 발생하면 최대 3번 재시도를 수행함을 나타냅니다.

고급 설정

  • 예외지정 : 모든 예외에 대하여 재시도를 수행하는 것은 아닙니다. 때로는 특정 예외가 일시적인 것으로 판별될 수도 있습니다. Spring Retry 를 사용하면 재시도를 트리거하는 예외를 지정할 수 있습니다.
@Retryable(value = {NetworkException.class, TimeoutException.class}, maxAttempts = 3)
public String someOperation() {
    // ... logic
}
  • 백 오프 전략 : 빠르게 연속해서 재시도를 실행할 경우 속도 제한과 같은 시나리오에서는 역효과를 발휘할 수 있습니다. 백오프 전략을 구현하면 재시도 사이에 지연을 발생시켜 다른 서비스나 시스템이 과부하가 걸릴 가능성을 줄일 수 있습니다.
@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 1000))
public String someOperationWithBackoff() {
    // ... logic
}

위의 예시 코드에서는, 재시도 사이에 1000 밀리세컨드(1초)의 지연이 발생합니다.

복구 메커니즘

재시도 후에도 작업이 계속 실패하는 경우는 어떻게 할까요?? Spring Retry는 복구 메커니즘을 제공하므로 개발자는 모든 재시도를 수행한 이 후 실행되는 대체 방법을 정의할 수 있습니다.

@Service
public class MyService {

    @Retryable(value = Exception.class, maxAttempts = 3)
    public String someOperation() {
        // ... logic that might fail
    }

    @Recover
    public String recover(Exception e) {
        return "Fallback data";
    }
}

위의 예시 코드에서 someOperation이 지속적으로 실패하는 경우, recover 메소드가 실행되어 지속적인 실패 상황에서도 항상 응답이나 조치가 이루어짐을 보장합니다.

Stateful vs. Stateless 재시도

기본적으로 Spring Retry 는 stateless 로 실행됩니다. 각각의 재시도는 이전 시도와 독립적입니다. 그러나 특정 시나리오에서, 특히 상태를 유지해야 하는 stateful 시스템이나 이전 실패를 알고있어야 하는 하는 경우, stateful 재시도가 유용할 수 있습니다. stateful 재시도를 구성하려면 @Retryable 어노테이션의 속성에서 stateful 을 true 로 설정해야 합니다.

Spring Retry를 마이크로서비스에 적용하면 일반적으로 개발자의 통제를 벗어나는 일시적인 장애가 서비스의 저하나 다운으로 이어지지 않도록 회복탄력성을 크게 향상시킵니다.

Spring Hystrix 를 사용한 Fallback 전략

Netflix의 Hystrix 라이브러리는 분산시스템의 회복탄력성을 높이는데 중요한 역할을 해 왔습니다. 주로 서킷브레이커 기능으로 알려져 있지만, 또다른 중요한 기능은 Fallback 메커니즘 입니다. Fallback 을 사용하면 특정 서비스 동작에 장애가 발생하더라도 잠재적으로 성능이 저하된 모드에서도 어플리케이션이 계속 작동할 수 있습니다.

개요

Fallback 은 메인 로직이 실패할 경우 대체 응답을 제공하는 것입니다. 이는 기본값을 반환하거나 다른 서비스를 호출하거나 또는 다른 보상 작업을 수행하는 등 여러가지 방식으로 이루어질 수 있습니다. Hystrix의 Fallback 기능은 오류가 발생하더라도 사용자가 오류대신 응답을 받을 수 있도록 보장합니다.

기본 사용법

Spring 프로젝트에서 Hystrix 를 사용하려면 Spring Cloud Starter Hystrix 를 디펜던시에 추가해야 합니다. 통합한 후에는 연관된 Fallback 기능에 Hystrix command 를 적용할 수 있습니다.

@Service
public class AnotherService {

    @HystrixCommand(fallbackMethod = "fallbackForOperation")
    public String riskyOperation() {
        // ... logic that might fail
    }

    public String fallbackForOperation() {
        return "Default Response";
    }
}

 riskyOperation 이 실패가 발생하면 대체 메소드인 fallbackForOperation 이 호출되어 기본 응답을 반환합니다.

고급 설정

  • 사용자 정의 Fallback : fallback 메소드를 지정하는 것 이외에도 Hystrix는 HystrixCommand 클래스를 사용하여 사용자 정의 Fallback을 생성할 수 있습니다. 이는 복잡한 시나리오를 다룰때 더 많은 유연성을 제공합니다.
  • Fallback과 예외 핸들링 : Fallback 메소드 자체가 안정적이고 예외를 발생시키지 않도록 해야 합니다. 필요한 경우 fallback 메소드 내에 추가로 try-catch 블럭이나 중첩된 fallback을 활용할 수 있습니다.

Fallback 의 이점

  • 사용자 경험 개선 : 사용자는 오류보다 기본값이나 캐시된 값을 받는것을 선호합니다. fallback은 잠재적인 오류를 보다 사용자 친화적인 응답이나 동작으로 변환할 수 있습니다.
  • 시스템 부하 감소 :  서비스나 작업이 실패하면 이미 부하가 많이 걸릴때가 많습니다. 지속적인 재시도나 오류처리는 상황을 악화시킬 수 있습니다. fallback 은 빠르게 대체응답을 제공함으로써 이러한 부하를 완화시킵니다.

제한사항

  • 오래된 데이터 : fallback 이 캐시 데이터에 으존하는 경우 사용자에게 과거나 오래된 정보를 반환할 위험이 있습니다. 
  • 과도한 의존 : fallback 은 오류처리에 적합하지만 과도하게 의존할 경우 근본적인 문제를 발견하기 어려울 수 있습니다. 빈번한 fallback 이 발생한 경우 근본 원인을 모니터링하고 해결하는 것이 중요합니다.

Fallback 메커니즘, 특히 재시도나 서킷브레이커와 같은 회복탄력성 패턴과 결합할 때, 분산 시스템의 견고성을 크게 향상시킬 수 있습니다. Hystrix가 널리 사용되어 왔지만, 현재 유지보수 모드에 있다는 점에 유의해야 합니다. Resilience4j 와 같은 대안이 추가 기능과 함께 유사한 기능을 제공하는 대안으로 떠오르고 있습니다.

향상된 회복탄력성을 위한 Retry와 Fallback 조합

재시도와 함께 fallback 을 조합하는 것은 안전망 위에 안전망을 더하는 것과 같습니다. 재시도 메커니즘을 사용하면 실패한 작업을 다시 시도하여 성공적인 결과를  기대할 수 있지만, fallback 은 모든 재시도가 실패하는 경우 플랜B 를 마련할 수 있도록 보장합니다.

계층화된 방어 접근 방식

재시도와 fallback을 조합하여 계층화된 방어를 생각해 보세요. 첫번째 방어선(재시도)은 일시적인 문제를 극복하는 것을 목표로 하고, 두번째 방어선(fallback)은 첫번째 방어선이 실패하더라도 상황을 적절하게 관리하고 완화할 수 있는 방법이 있음을 보장합니다.

Spring 에 적용

  • Spring Retry 와 Hystrix 함께 사용 : Spring Retry 가 재시도를 용이하게 하는 반면, Hystrix 는 fallback 을 처리하는 데 사용할 수 있습니다.
@Service
public class ResilientService {

    @Retryable(value = Exception.class, maxAttempts = 3)
    @HystrixCommand(fallbackMethod = "fallbackMethod")
    public String operation() {
        // ... logic that might fail
    }

    public String fallbackMethod() {
        return "Fallback Response";
    }
}

이 예제에서는 작업이 실패하면 Spring Retry 가 최대 3번까지 재시도합니다. 모든 시도가 실패하면 Hystrix의 fallbackMethod 가 호출됩니다.

  • 상태 관리 : 재시도 및 fallback 을 조합할때, 특히 재시도가 stateful 인 경우 이러한 메커니즘 간의 상태를 관리하는 것이 중요합니다. 발생한 오류 등 이전 시도에 대한 정보는 적절한 fallback 전략을 결정하는데 유용할 수 있습니다. 

장점

  • 신뢰성 향상 : 두 개의 방어선이 있으므로, 기본 응답이나 캐시된 응답이라도 성공적인 응답을 제공할 가능성이 높아집니다. 
  • 사용자 경험 개선 : 최종 사용자는 일시적인 시스템 결함이나 과부하로부터 보호되므로 거의 모든 시나리오에서 응답을 받을 수 있습니다.
  • 시스템 부하 감소 : 계층화된 접근 방식을 사용하면 다른 서비스나 시스템에 과부하를 주지 않습니다. 몇 번의 재시도 후 fallback 으로 전환하면 잠재적으로 문제가 발생할 수 있는 시스템에 지속적인 부하가 걸리는 것을 방지할 수 있습니다.

고려사항

  • 재시도 횟수 결정 : 작업을 재시도할 횟수를 결정할 때 균형을 맞추는 것이 중요합니다. 재시도 횟수가 너무 많으면 시스템에 부담이 생길 수 있고, 너무 적으면 일시적인 문제를 극복할 수 있는 기회를 놓칠 수 있습니다. 
  • 동적 fallback : 정적 fallback 이 항상 적절한 것은 아닙니다. 장애의 성격이나 호출되는 특정 서비스에 따라 동적 fallback 로직이 더 가치 있는 응답을 제공할 수 있습니다. 

재시도와 fallback 를 조합하면 견고한 내구성 전략을 제공하여, 다중 실패에도 시스템이 효과적으로 대응할 수 있도록 보장합니다.

결론

마이크로서비스에서 회복탄력성을 구축하는 것은 필수입니다. 재시도 및 폴백 등 Spring 에코시스템에서 제공하는 도구와 메커니즘은 서비스가 장애를 자연스럽게 처리할 수 있도록 보장합니다. 각 메커니즘의 세부사항을 이해하고 강점을 활용함으로써, 시스템을 견고하게 할 뿐 아니라 최종 사용자에게 원활한 경험을 제공하는 시스템을 설계할 수 있습니다.