SwiftUI Circuit Breakers & Network Resilience Patterns (연쇄 실패 방지)
Source: Dev.to
대부분의 앱은 다음과 같이 API를 호출합니다:
try await api.fetchUser()이렇게 하면 동작하지만… 서버가 오류를 일으키기 시작하면 문제가 발생합니다. 그때 앱은 다음과 같은 일을 할 수 있습니다:
- 즉시 재시도
- 수백 개의 실패 요청을 전송
- 배터리 소모
- 백엔드 과부하
- UI 속도 저하
- 장애 악화
이를 cascading failure 라고 합니다. 프로덕션 앱은 보호 메커니즘이 필요합니다.
🧠 핵심 원칙
시스템에 문제가 발생하면 상황을 악화시키지 마세요. 무한히 재시도하는 대신, 시스템은 실패를 감지하고 요청을 일시 중지해야 합니다.
🧱 1. 회로 차단기란?
회로 차단기는 실패하는 서비스에 대한 반복 호출을 방지합니다. 세 가지 상태가 있습니다:
Closed → Normal operation
Open → Requests blocked
Half-Open → Testing recoveryFlow
Requests Fail
→ Circuit Opens
→ Requests Blocked
→ Recovery Test
→ Circuit Closes🔌 2. 회로 상태 정의
enum CircuitState {
case closed
case open(until: Date)
case halfOpen
}- Closed → 요청 허용
- Open → 요청이 일시적으로 차단됨
- Half‑Open → 복구 테스트를 위해 제한된 요청 허용
final class CircuitBreaker {
private(set) var state: CircuitState = .closed
private var failureCount = 0
private let failureThreshold = 5
private let timeout: TimeInterval = 30
func recordFailure() {
failureCount += 1
if failureCount >= failureThreshold {
state = .open(until: Date().addingTimeInterval(timeout))
}
}
func recordSuccess() {
failureCount = 0
state = .closed
}
}실패가 누적되면 회로가 열립니다.
🚦 4. 회로가 열려 있을 때 요청 차단
요청을 수행하기 전에:
func canExecute() -> Bool {
switch state {
case .closed:
return true
case .open(let until):
return Date() > until
case .halfOpen:
return true
}
}사용법
guard breaker.canExecute() else {
throw NetworkError.circuitOpen
}이는 백엔드를 보호합니다.
🔄 5. 반열림 상태로 전환
When the timeout expires:
case .open(let until):
if Date() > until {
state = .halfOpen
return true
}Only a small number of requests should test recovery. If they succeed → close the circuit; if they fail → reopen.
🔋 6. 모바일 앱에 회로 차단기가 필요한 이유
모바일 네트워크는 불안정합니다. 일반적인 장애 시나리오는 다음과 같습니다:
- 서버 장애
- DNS 문제
- TLS 실패
- 속도 제한
- 셀룰러 패킷 손실
회로 차단기가 없으면 앱이 다음과 같은 일을 할 수 있습니다:
- 백엔드에 스팸 요청을 보냄
- 배터리를 빠르게 소모함
- 장애를 확대함
회로 차단기는 이를 방지합니다.
🧱 7. API 클라이언트와 통합하기
Wrap network calls:
func performRequest(_ operation: () async throws -> T) async throws -> T {
guard breaker.canExecute() else {
throw NetworkError.circuitOpen
}
do {
let result = try await operation()
breaker.recordSuccess()
return result
} catch {
breaker.recordFailure()
throw error
}
}이렇게 하면 복원력이 자동으로 적용됩니다.
🌐 8. 재시도 전략과 결합하기
Circuit breakers work with retries. Example flow:
Request
→ Failure
→ Retry (exponential backoff)
→ Failure threshold reached
→ Circuit opens
→ Requests pausedThis prevents retry storms.
🧪 9. 회로 차단기 테스트
테스트 시나리오:
- 반복적인 서버 오류
- 타임아웃 폭풍
- 네트워크 연결 끊김
- 장애 후 복구
- 반열림 전환
검증 내용:
- 장애 발생 시 요청이 중단되는지
- 복구 후 요청이 재개되는지
⚠️ 10. Common Anti‑Patterns
Avoid:
- infinite retries → 무한 재시도
- retrying immediately after failure → 실패 직후 즉시 재시도
- ignoring server rate limits → 서버 속도 제한 무시
- retrying while offline → 오프라인 상태에서 재시도
- not tracking failure counts → 실패 횟수 추적 안 함
These cause backend overload, cascading outages, and battery drain. → 이러한 행동은 백엔드 과부하, 연쇄 장애, 배터리 소모를 초래합니다.
🧠 멘탈 모델
Request
→ Failure Detection
→ Circuit Breaker
→ Pause Requests
→ Recovery Probe
→ Resume Traffic주의: “그냥 다시 시도하세요.”
🚀 최종 생각
Circuit breakers는 앱에 다음과 같은 이점을 제공합니다:
- 백엔드 보호
- 보다 스마트한 재시도 동작
- 배터리 사용량 감소
- 정전 시 복원력
- 예측 가능한 복구
이는 취약한 네트워크 클라이언트와 프로덕션 급 모바일 시스템의 차이점입니다.