Polly와 함께 탄력적인 .NET 애플리케이션 구축
Source: Dev.to
현대 애플리케이션은 거의 독립적으로 존재하지 않습니다. 데이터베이스, 서드파티 API, 마이크로서비스, 그리고 클라우드 리소스와 지속적으로 통신합니다.
이 때문에 실패는 예외가 아니라 기대치입니다.
일시적인 네트워크 문제, 일시적인 서비스 중단, 혹은 짧은 타임아웃은 복원력을 고려하지 않을 경우 잘 설계된 시스템조차 쉽게 무너뜨릴 수 있습니다.
이 글에서는 다음을 살펴볼 것입니다:
- 소프트웨어 시스템에서 복원력(resilience) 이 의미하는 바
- 일반적인 복원력 패턴들
- Polly를 사용하여 .NET에서 Retry와 Circuit Breaker 패턴을 구현하는 방법
소프트웨어 시스템에서 회복탄력성이란?
회복탄력성은 애플리케이션이 다음을 할 수 있는 능력입니다:
- 실패를 우아하게 처리한다
- 일시적인 문제에서 자동으로 복구한다
- 가용성과 응답성을 유지한다
무언가 잘못되어도 시스템이 충돌하거나 사용자를 차단하지 않고, 회복탄력적인 시스템은 적응하여 계속 작동합니다. 회복탄력성 메커니즘을 안전망이라고 생각하세요 — 이는 애플리케이션이 불가피하게 실패할 때 잡아줍니다.
일시적 실패 이해
**일시적 실패(transient failure)**는 짧은 시간 후에 스스로 해결되는 일시적인 문제를 말합니다. 흔한 예시로는 다음과 같습니다:
- 일시적인 네트워크 오류
- API 타임아웃
- 단기간에 발생하는 데이터베이스 연결 실패
- 클라우드 서비스 제한(스로틀링)
이러한 실패는 영구적인 오류로 간주해서는 안 됩니다. 짧은 지연 후에 작업을 다시 시도하면 성공할 가능성이 높습니다.
재시도 패턴 (Retry Pattern)
재시도 패턴이란?
Retry Pattern은 실패한 작업을 제한된 횟수만큼 자동으로 다시 실행하고, 그 이후에 포기합니다.
언제 재시도를 사용해야 할까요?
- 네트워크 호출
- 외부 API
- 메시지 큐
- 클라우드 리소스
언제 재시도를 사용하면 안 될까요?
- 검증 오류
- 인증 실패
- 비즈니스 규칙 위반
Polly를 이용한 재시도 예제
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(2),
onRetry: (exception, timeSpan, retryCount, context) =>
{
Console.WriteLine($"Retry {retryCount} after {timeSpan.TotalSeconds}s");
});
이 코드가 하는 일
- 요청을 최대 3번 재시도합니다
- 재시도 사이에 2초 대기합니다
HttpRequestException이 발생했을 때만 재시도합니다
이 접근 방식은 일시적인 오류에 대한 성공률을 크게 높이며 사용자 경험에 미치는 영향을 최소화합니다.
Source: …
회로 차단기 패턴
회로 차단기가 해결하는 문제
제3자 서비스를 호출했는데 완전히 다운된 경우를 상상해 보세요:
- 요청이 계속 실패한다
- 스레드가 차단된 상태로 남는다
- 시스템 자원이 고갈된다
- 실패가 시스템 전체로 전파된다
이것이 작은 실패가 시스템 전체 장애로 이어지는 방식입니다.
회로 차단기란?
회로 차단기는 애플리케이션이 실패할 가능성이 높은 작업을 실행하지 않도록 방지합니다. 전기 회로 차단기와 유사하게 동작합니다:
- 반복적인 실패를 감지한다
- 일시적으로 요청 전송을 중단한다
- 시스템이 회복될 수 있도록 한다
회로 차단기 상태
| 상태 | 설명 |
|---|---|
| Closed | 요청이 정상적으로 흐른다. |
| Open | 요청이 즉시 거부된다 (fail‑fast). |
| Half‑Open | 복구 여부를 확인하기 위해 제한된 수의 테스트 요청을 허용한다. |
Polly를 이용한 회로 차단기 예제
var circuitBreakerPolicy = Policy
.Handle<HttpRequestException>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: TimeSpan.FromSeconds(30),
onBreak: (exception, duration) =>
{
Console.WriteLine("Circuit opened");
},
onReset: () =>
{
Console.WriteLine("Circuit closed");
},
onHalfOpen: () =>
{
Console.WriteLine("Circuit half-open");
});
이 코드가 하는 일
- 연속 5번 실패가 발생하면 회로를 연다
- 30초 동안 요청을 차단한다
- 차단 기간이 끝난 뒤 자동으로 복구를 시도한다
재시도와 회로 차단기 결합
재시도만 사용하면 실패하는 서비스를 과부하 시킬 수 있습니다.
회로 차단기만 사용하면 과도하게 공격적일 수 있습니다.
실제 힘은 이들을 결합할 때 나옵니다.
var resiliencePolicy = Policy.WrapAsync(
retryPolicy,
circuitBreakerPolicy);
실행 예시
await resiliencePolicy.ExecuteAsync(async () =>
{
var response = await httpClient.GetAsync("https://external-api.com/data");
response.EnsureSuccessStatusCode();
});
이 설정은 다음을 보장합니다:
- 일시적인 실패는 재시도됩니다
- 지속적인 실패는 회로 차단을 트리거합니다
- 시스템 안정성이 유지됩니다
왜 Polly인가?
Polly는 .NET 생태계에서 사실상의 복원력 라이브러리입니다. 이유는:
- 유창하고 표현력 있는 API
- 비동기 및 동기 작업 지원
HttpClientFactory와 쉽게 통합- 프로덕션 시스템에서 널리 사용
지원되는 패턴에는 다음이 포함됩니다:
- 재시도
- 회로 차단기
- 타임아웃
- 폴백
- 벌크헤드 격리
실제 사용 사례
Polly는 특히 다음에 유용합니다:
- 마이크로서비스 통신
- 금융 및 트레이딩 시스템
- 클라우드‑네이티브 애플리케이션
- API 게이트웨이
- SignalR 및 실시간 시스템
Any place where I/O 또는 원격 호출이 존재하는 모든 곳에서 복원력은 일급 고려사항이어야 합니다.
핵심 요점
- 분산 시스템에서는 실패가 불가피합니다
- 회복력은 작은 문제가 중단으로 이어지는 것을 방지합니다
- 재시도는 일시적인 오류를 처리합니다
- 회로 차단기는 시스템 안정성을 보호합니다
- Polly는 .NET에서 회복력을 실용적이고 프로덕션 준비된 형태로 만들어 줍니다
최종 생각
복원력은 선택적인 기능이 아니라 현대 애플리케이션을 위한 설계 요구사항입니다. Polly와 같은 견고한 라이브러리를 사용해 Retry 및 Circuit Breaker와 같은 패턴을 구현하면 프로세스 외부의 환경이 비정상적일 때도 시스템을 가용하고, 반응성이 뛰어나며, 유지보수가 용이하도록 구축할 수 있습니다.
결론
검증된 패턴과 Polly 같은 라이브러리를 활용하면 다음과 같은 시스템을 구축할 수 있습니다:
- 안정적
- 확장 가능
- 내결함성
- 사용자 친화적
애플리케이션이 외부와 통신한다면, 탄력적이어야 합니다.
저는 Morteza Jangjoo이며 “누군가 나에게 설명해 주었으면 하는 것들을 설명합니다.”