SwiftUI 레이트 제한 및 백프레셔 (자신의 앱으로부터 백엔드 보호)
Source: Dev.to
대부분의 앱은 API를 다음과 같이 호출합니다:
try await api.fetchFeed()
이것은 정상적으로 동작합니다… 하지만 한 번에 많은 요청이 발생하면 문제가 생깁니다.
예시
- 무한 새로고침 루프
- 과도한 백그라운드 동기화
- 동일한 데이터를 요청하는 여러 뷰
- 네트워크 복구 후 발생하는 재시도 폭풍
- 동시에 동기화되는 여러 기기
갑자기 여러분의 앱이 자체 백엔드에 가장 큰 위협이 됩니다.
🧠 핵심 원칙
건강한 시스템은 요청 속도를 제어합니다. 모든 것이 정상적으로 작동하더라도, 제어되지 않은 트래픽은 API를 과부하시킬 수 있습니다.
🧱 1. Rate Limiting이란?
Rate limiting은 일정 시간 창에서 발생할 수 있는 요청 수를 제어합니다.
예시 규칙
10 requests per second
더 많은 요청이 들어오면 대기, 대기열에 넣기, 혹은 거부해야 합니다. 이는 백엔드와 클라이언트 장치를 모두 보호합니다.
🧬 2. 토큰 버킷 모델
일반적인 접근 방식은 토큰 버킷 알고리즘입니다.
개념
Bucket contains tokens
Each request consumes one token
Tokens refill over time
버킷이 비어 있으면, 요청은 대기해야 합니다.
🧱 3. 간단한 속도 제한기 구현
actor RateLimiter {
private let maxTokens: Int
private let refillInterval: TimeInterval
private var tokens: Int
private var lastRefill: Date
init(maxTokens: Int, refillInterval: TimeInterval) {
self.maxTokens = maxTokens
self.refillInterval = refillInterval
self.tokens = maxTokens
self.lastRefill = Date()
}
func acquire() async {
refill()
while tokens == 0 {
try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 s
refill()
}
tokens -= 1
}
private func refill() {
let now = Date()
let elapsed = now.timeIntervalSince(lastRefill)
if elapsed >= refillInterval {
tokens = maxTokens
lastRefill = now
}
}
}
이렇게 하면 요청이 제어된 속도로 이루어집니다.
🚦 4. Using the Rate Limiter
Wrap your API calls:
let limiter = RateLimiter(maxTokens: 5, refillInterval: 1)
func fetchFeed() async throws -> Feed {
await limiter.acquire()
return try await api.fetchFeed()
}
Now your app cannot exceed 5 requests per second.
🔄 5. 백프레셔란 무엇인가?
Backpressure는 시스템이 처리할 수 있는 것보다 더 많은 작업을 생성하는 것을 방지합니다.
시나리오
스크롤 뷰가 50개의 이미지 로드를 트리거함
백프레셔 없이: 50개의 네트워크 요청이 즉시 발생합니다.
백프레셔와 함께: 요청이 큐에 쌓여 점진적으로 실행됩니다.
이는 CPU, 메모리, 네트워크 및 백엔드 API를 보호합니다.
📦 6. 요청 큐 패턴
actor RequestQueue {
private var queue: [() async -> Void] = []
func enqueue(_ task: @escaping () async -> Void) {
queue.append(task)
processNext()
}
private func processNext() {
guard !queue.isEmpty else { return }
let task = queue.removeFirst()
Task {
await task()
processNext()
}
}
}
이 패턴은 대기 중인 작업들의 제어된 실행을 보장합니다.
🔋 7. Why Mobile Apps Need Rate Limiting
모바일 환경은 예측하기 어렵습니다. 일반적인 문제는 다음과 같습니다:
- API 할당량
- 느린 셀룰러 연결
- 백그라운드 동기화 급증
- 다중 탭 새로 고침 루프
제한이 없으면 백엔드가 과부하되고 배터리 소모가 빨라지며 UI가 불안정해집니다. 속도 제한은 시스템을 안정화시킵니다.
🌐 8. 회로 차단기와 결합하기
Circuit Breakers → stop requests during failures
Rate Limiting → control requests during success
함께하면 완전한 네트워크 복원력을 형성합니다.
🧪 9. 속도 제한 테스트
테스트 시나리오
- 빠른 새로고침 루프
- 네트워크 호출을 트리거하는 대형 스크롤 리스트
- 백그라운드 동기화 폭발
- 재시도 폭풍
검증 항목
- 요청 속도가 안정적으로 유지되는지
- 큐가 올바르게 처리되는지
- 백엔드 과부하가 발생하지 않는지
⚠️ 10. 공통 안티 패턴
- 무제한 병렬 요청
- 실패 후 즉시 재시도
- 조정 없이 뷰당 네트워크 호출
- 서버 속도 제한 무시
- 동일 요청 중복 제거 안 함
이러한 문제는 요청 폭풍, API 차단, 배터리 소모, 백엔드 불안정을 초래합니다.
🧠 멘탈 모델
User Action
→ Request Queue
→ Rate Limiter
→ Network Request
→ API
Not: “Fire every request immediately.” → 주의: “모든 요청을 즉시 전송하지 마세요.”
🚀 최종 생각
- 예측 가능한 네트워크 동작
- 백엔드 보호
- 원활한 성능
- 배터리 사용 효율 향상
- 프로덕션 수준의 복원력