4.2 KiB
4.2 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ios-network-urlsession-patterns | URLSession 네트워킹 실전 패턴 | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
URLSession 네트워킹 실전
URLSession 직접 쓰면 OK. 단 (1) Endpoint 추상화, (2) decoding 통합, (3) 인증 헤더 자동, (4) 재시도/취소 4가지가 있어야 production-grade.
📖 핵심 개념
- 단일
URLSession인스턴스 (custom config) — 테스트 + 실제 분리. URLRequestbuilder 또는 Endpoint enum.JSONDecoder공유 (snake_case 변환 등).- async/await 가 기본 —
data(for:),data(from:).
💻 코드 패턴
Endpoint 추상화
struct Endpoint<Response: Decodable> {
let path: String
let method: String
let body: Data?
}
extension Endpoint where Response == User {
static func user(id: String) -> Endpoint<User> {
Endpoint(path: "/users/\(id)", method: "GET", body: nil)
}
}
APIClient
actor APIClient {
private let session: URLSession
private let baseURL: URL
private let decoder: JSONDecoder
private var token: String?
init(baseURL: URL, session: URLSession = .shared) {
self.session = session
self.baseURL = baseURL
self.decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
decoder.dateDecodingStrategy = .iso8601
}
func setToken(_ t: String?) { self.token = t }
func send<R>(_ ep: Endpoint<R>) async throws -> R {
var req = URLRequest(url: baseURL.appendingPathComponent(ep.path))
req.httpMethod = ep.method
req.httpBody = ep.body
req.setValue("application/json", forHTTPHeaderField: "Content-Type")
if let token { req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") }
let (data, resp) = try await session.data(for: req)
guard let http = resp as? HTTPURLResponse else { throw APIError.invalidResponse }
guard (200..<300).contains(http.statusCode) else {
throw APIError.status(http.statusCode, data)
}
return try decoder.decode(R.self, from: data)
}
}
// 사용
let user = try await api.send(.user(id: "1"))
Cancellation
let task = Task { try await api.send(.user(id: "1")) }
// view disappear
task.cancel()
Retry with backoff
func withRetry<T>(_ op: @escaping () async throws -> T,
max: Int = 3) async throws -> T {
var last: Error?
for attempt in 0..<max {
do { return try await op() }
catch let e where shouldRetry(e) {
last = e
try await Task.sleep(nanoseconds: UInt64(pow(2.0, Double(attempt)) * 1e9))
}
}
throw last ?? APIError.unknown
}
🤔 의사결정 기준
| 상황 | 권장 |
|---|---|
| 단순 앱 (10 endpoint 내외) | URLSession + Endpoint enum |
| 복잡 / GraphQL | Apollo / Relay |
| Reactive (Combine) | URLSession.dataTaskPublisher |
| 배경 다운로드 | URLSession background config |
| 실시간 | URLSessionWebSocketTask 또는 Starscream |
❌ 안티패턴
URLSession.shared에 직접 의존: 테스트 어려움. 주입.- status code 검증 누락: 4xx/5xx body 를 모델로 decode 시도 → 잘못된 에러 메시지.
- JSON 키 case 불일치 처리 안 함: snake_case 응답 + camelCase struct → decode 실패. keyDecodingStrategy.
- Date decoding 기본 (UNIX timestamp): API 가 ISO8601 이면 깨짐. 명시.
- 에러를 그냥 print: 디버깅 어려움. 구조화 로깅 + Sentry.
- Background 작업을 일반 dataTask 로: 앱 suspend 시 끊김. background config 또는 URLSession.shared.
- 인증 토큰 만료 처리 누락: 401 에 그냥 fail. 401 → refresh → 재시도 로직.
🤖 LLM 활용 힌트
- Endpoint enum + APIClient actor 권장.
- 401 → refresh → retry 자동화 명시.