---
id: ios-network-urlsession-patterns
title: URLSession 네트워킹 실전 패턴
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [ios, networking, urlsession, swift, vibe-coding]
tech_stack: { language: "Swift", applicable_to: ["iOS", "macOS"] }
applied_in: []
aliases: [networking layer, URLRequest, decoding, retry]
---

# URLSession 네트워킹 실전

> URLSession 직접 쓰면 OK. 단 **(1) Endpoint 추상화, (2) decoding 통합, (3) 인증 헤더 자동, (4) 재시도/취소** 4가지가 있어야 production-grade.

## 📖 핵심 개념
- 단일 `URLSession` 인스턴스 (custom config) — 테스트 + 실제 분리.
- `URLRequest` builder 또는 Endpoint enum.
- `JSONDecoder` 공유 (snake_case 변환 등).
- async/await 가 기본 — `data(for:)`, `data(from:)`.

## 💻 코드 패턴

### Endpoint 추상화
```swift
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
```swift
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
```swift
let task = Task { try await api.send(.user(id: "1")) }
// view disappear
task.cancel()
```

### Retry with backoff
```swift
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 자동화 명시.

## 🔗 관련 문서
- [[iOS_Swift_Concurrency_async_await]]
- [[iOS_Swift_Result_Type]]
- [[Web_JWT_Patterns]]
