Computer >> 컴퓨터 >  >> 프로그래밍 >> C#

C#에서 재시도(Retry) 로직을 구현하는 방법

네트워크 호출이나 외부 서비스 연동과 같이 일시적인 장애가 발생할 수 있는 작업에는 재시도(Retry) 로직을 적용하는 것이 좋습니다. 재시도 로직은 작업이 실패했을 때 즉시 포기하지 않고, 일정 횟수만큼 자동으로 다시 시도함으로써 애플리케이션의 안정성과 복원력(resilience)을 높여줍니다.

재시도 로직 구현 시 핵심 원칙

재시도 로직은 단순히 반복문을 돌리는 것 이상으로 신중하게 설계해야 합니다. 다음 두 가지 원칙을 반드시 기억하세요.

1. 실패 작업의 전체 맥락을 아는 곳에만 적용

재시도 로직은 해당 작업이 실패한 전체 맥락(full context)을 파악할 수 있는 위치에서만 구현해야 합니다. 예를 들어, 쓰기 작업이 실제로 성공했는지 여부를 알 수 없는 상황에서 무조건 재시도하면 데이터 중복이나 부작용이 발생할 수 있습니다.

2. 모든 연결 실패는 반드시 로깅

재시도를 유발하는 모든 연결 실패(connectivity failure)는 반드시 로그로 남겨야 합니다. 이를 통해 애플리케이션, 서비스 또는 리소스의 근본적인 문제를 추적하고 원인을 분석할 수 있습니다.

구현 예제

아래 예제는 RetryHelper라는 정적 클래스를 통해 지정된 횟수만큼 작업을 재시도하는 간단한 패턴을 보여줍니다. 각 시도 사이에는 설정된 지연 시간(여기서는 2초)만큼 대기합니다.

class Program{
    public static void Main(){
        HttpClient client = new HttpClient();
        dynamic res = null;
        var retryAttempts = 3;
        var delay = TimeSpan.FromSeconds(2);

        RetryHelper.Retry(retryAttempts, delay, () =>{
            res = client.GetAsync("https://example22.com/api/cycles/1");
        });

        Console.ReadLine();
    }
}

public static class RetryHelper{
    public static void Retry(int times, TimeSpan delay, Action operation){
        var attempts = 0;
        do{
            try{
                attempts++;
                System.Console.WriteLine(attempts);
                operation();
                break;
            }
            catch (Exception ex){
                if (attempts == times)
                    throw;
                Task.Delay(delay).Wait();
            }
        } while (true);
    }
}

코드 동작 방식

  • 재시도 횟수와 지연 시간 설정: retryAttempts = 3은 최대 3번까지 시도한다는 의미이며, delay는 각 시도 사이의 대기 시간입니다.
  • 작업 실행: Action 델리게이트로 전달된 HTTP GET 요청이 실행됩니다.
  • 예외 처리: 작업이 성공하면 break로 루프를 종료하고, 실패하면 마지막 시도인 경우 예외를 다시 던져(throw) 호출자에게 알립니다.
  • 대기 후 재시도: 마지막 시도가 아니라면 Task.Delay(delay).Wait()로 지정된 시간만큼 기다린 뒤 다시 시도합니다.

추가 고려 사항

실제 프로덕션 환경에서는 위와 같은 직접 구현보다 Polly 같은 검증된 탄력성(resilience) 라이브러리를 사용하는 것도 좋은 선택입니다. Polly는 지수 백오프(exponential backoff), 회로 차단기(circuit breaker), 타임아웃 등 다양한 재시도 정책을 손쉽게 구성할 수 있게 해줍니다. 또한 비동기 메서드(async/await)를 사용하는 경우 스레드를 차단하는 .Wait() 대신 비동기 방식으로 대기하는 것이 성능 면에서 바람직합니다.