실제로는 URLSession은 Apple이 만든 거대한 URL Loading System의 하나인 거고 그 안에는 여러 라이브러리들이 있어요:)
URL Loading System의 전체 구조
URL Loading System의 핵심 특징:
- 비동기 처리: 모든 네트워크 작업이 백그라운드에서 수행되어 앱의 반응성 유지
- 프로토콜 지원: HTTP, HTTPS, FTP, 파일, 그리고 커스텀 프로토콜까지 지원
Cookie Storage (쿠키 관리부서 🍪)
- HTTPCookieStorage: 웹사이트 로그인 상태 유지, 사용자 설정 기억 등을 위한 쿠키 자동 관리
- HTTPCookie: 개별 쿠키 정보를 담는 컨테이너 역할
Authentication & Credentials (보안 인증부서 🔐)
- URLProtectionSpace: 서버별, 인증 방식별로 보안 영역을 구분하는 역할
- URLCredentialStorage: 로그인 정보를 안전하게 보관하여 재입력 없이 자동 인증 처리
- URLCredential: 사용자명/비밀번호, 인증서 등 실제 인증 데이터를 담는 역할
- URLAuthenticationChallenge: 서버의 인증 요구에 대응하여 적절한 인증 방식 선택 및 처리
Cache Management (캐시 관리부서 💾)
- URLCache: 네트워크 부하 감소와 응답 속도 향상을 위한 응답 데이터 캐싱
- CachedURLResponse: 캐시된 응답과 메타데이터를 하나로 묶어서 관리.
Protocol Support (프로토콜 지원부서 🔧)
- URLProtocol: 네트워크 요청/응답을 가로채서 커스텀 로직 삽입 (테스트용 Mock 데이터, 로깅, 캐싱 등)
- 이쪽은 사실 개발할때 많이 다룰 수 있는 부분이에요. 특히 Mock 데이터로 로직을 테스트하거나 모든 네트워크 요청을 자동으로 로깅하는 프로토콜을 만들 수 있고 네트워크가 없을 때 캐시된 데이터를 반환하도록 프로토콜을 만들수 도 있습니다.
class LoggingURLProtocol: URLProtocol {
override class func canInit(with request: URLRequest) -> Bool {
// 무한루프 방지
return property(forKey: "LoggingHandled", in: request) == nil
}
override func startLoading() {
// 요청 로깅
print("🌐 [REQUEST] \(request.httpMethod ?? "GET") \(request.url?.absoluteString ?? "")")
// 실제 네트워크 요청 실행
let mutableRequest = request.mutableCopy() as! NSMutableURLRequest
URLProtocol.setProperty(true, forKey: "LoggingHandled", in: mutableRequest)
let task = URLSession.shared.dataTask(with: mutableRequest as URLRequest) { data, response, error in
if let httpResponse = response as? HTTPURLResponse {
print("✅ [RESPONSE] \(httpResponse.statusCode)")
}
if let data = data {
print("📦 [DATA] \(data.count) bytes")
}
// 응답 전달...
}
task.resume()
}
}
// 활성화
URLProtocol.registerClass(LoggingURLProtocol.self)
이 코드는 모든 네트워크 요청을 자동으로 로깅하는 방법입니다.
결국 URLSession이 사장님이고, 나머지 팀들이 각자의 전문 분야를 담당하면서 조화롭게 협력하는 구조인 거라고 이해하면 더 쉬워요!
URLSession
URLSession은 크게 세션과 테스크 두가지 개념으로 구성됩니다.
세션(Session)이란?🤔🤔
논리적인 연결 단위
- 카페에 들어가서 (연결 설정)
- 아메리카노 주문하고 (첫 번째 요청)
- 쿠키도 추가 주문하고 (두 번째 요청)
- 계산하고 나가기 (세션 종료)
이 모든 과정이 하나의 세션 안에서 일어나는 거죠!
인증과 세션의 관계
여기서 중요한 포인트가 있어요. 세션이 제대로 시작되려면 인증이 선행되어야 한다는 거예요.
웹사이트에 로그인하면 서버가 "아, 이 사람이구나!" 하고 세션 ID를 발급해주잖아요?
그 이후부터는 그 ID만 보여주면 "이미 인증된 사용자"로 인식하는 거죠.
URLSession 타입별 특성과 선택 기준
1. Shared Session (URLSession.shared): 쿠키나 캐시를 다른 곳과 공유하기 때문에 민감한 인증 정보가 필요한 작업에는 사용하지 않는 게 좋아요.
2. Default Session: Shared Session과 비슷하지만 델리게이트 설정이나 커스터마이징이 가능해요.
3. Ephemeral Session: 시크릿 모드 같은 거예요! 캐시, 쿠키, 인증 정보를 디스크에 저장하지 않고 메모리에만 보관합니다.
4. Background Session: 앱이 백그라운드에 있어도 다운로드/업로드 계속 진행
URLSessionConfiguration이란?
Configuration을 설정 객체라고 생각하면 돼요. 근데 중요한 건 URLSession 생성 전에 미리 설정해야 한다는 점이에요!
왜 미리 설정해야 할까요?
- 세션 생성 시 설정 복사: URLSession이 만들어지면 Configuration 설정들을 내부적으로 복사해서 저장해요
- 변경 불가: 한 번 만들어진 세션은 설정을 바꿀 수 없어요. 새로운 정책이 필요하면 새 세션을 만들어야 해요
- Request 레벨 오버라이드: 일부 설정은 개별 URLRequest에서 덮어쓸 수 있지만, 세션 설정이 더 제한적인 경우엔 완화할 수 없어요
예를 들어 세션에서 allowsCellularAccess = false로 셀룰러를 금지했다면, 개별 요청에서 허용할 수 없는 거죠
주요 Configuration 속성들은 뭐가 있을까요?🤔
타임아웃 설정
let configuration = URLSessionConfiguration.default
// 새 데이터를 기다리는 타임아웃 (기본값: 60초)
configuration.timeoutIntervalForRequest = 30.0
// 전체 리소스 요청 완료 타임아웃 (기본값: 7일!)
configuration.timeoutIntervalForResource = 300.0
네트워크 정책 설정
// 셀룰러 네트워크 사용 허용 (기본값: true)
configuration.allowsCellularAccess = false
// 제한된 네트워크 사용 허용 (저전력 모드 등, iOS 13+)
configuration.allowsConstrainedNetworkAccess = false
// 비싼 네트워크 사용 허용 (로밍 등, iOS 13+)
configuration.allowsExpensiveNetworkAccess = false
// 연결성 대기 (iOS 11+, 기본값: false)
configuration.waitsForConnectivity = true
비싼 네트워크 같은 경우는 로밍을 말합니다.
waitsForConnectivity는 모바일의 특성상 네트워크가 불안정할수 있는데 네트워크 요청이 즉시 실패하지 않고 연결이 복원될 때까지 대기하도록 합니다.
캐시 정책 설정
// 캐시 정책 설정
configuration.requestCachePolicy = .useProtocolCachePolicy
// 커스텀 캐시 설정
let cache = URLCache(memoryCapacity: 50_000_000, diskCapacity: 100_000_000)
configuration.urlCache = cache
// 캐시 비활성화
configuration.urlCache = nil
캐시를 어떻게 사용할지 정책을 설정하는 곳입니다. 여기서 어떤 URLCache 인스턴스를 사용할지 지정하고 언제 캐시할지를 정하는 거죠
여기서 저기 .useProtocolCachePolicy는 Default인데 서버의 헤더를 따른다는 것을 의미합니다.
이외에도 캐시 정책에는 캐시를 무시하거나 캐시 우선 방식도 있습니다:)
HTTP 헤더 설정
// 모든 요청에 공통 헤더 추가
configuration.httpAdditionalHeaders = [
"User-Agent": "MyApp/1.0",
"Accept-Language": "ko-KR,ko;q=0.9"
]
URLSessionTask! 🔨
세션이 만들어지면, 세션 객체를 통해 실제로 데이터를 주고받는 작업 단위인 태스크를 생성합니다.
- NSURLSessionDataTask: JSON이나 XML과 같은 데이터를 주고받는 데 사용됩니다.
- NSURLSessionUploadTask: 파일이나 데이터를 업로드하는 데 사용됩니다. 서버 응답에 데이터가 포함될 수 있으므로 NSURLSessionDataTask를 상속받습니다.
- NSURLSessionDownloadTask: 파일 다운로드 전용이에요. 완료되면 임시 파일 경로를 반환하는데, 개발자가 이를 영구적인 위치로 옮겨야 해요
생각해보면 메모리에 바로 올리는 방법보다 파일로 받고 필요에 따라 메모리에 할당하는 방식이 때로는 좋겟네요:)
URLSession 실행 흐름
- 세션 설정 생성: URLSessionConfiguration 객체를 만들어서 세션 동작 방식을 정의
- 세션 생성: 설정을 기반으로 URLSession 인스턴스를 생성
- 태스크 객체 생성: 원하는 태스크를 만들고 요청할 URL을 지정
- 요청 전송: 태스크를 실행(resume())해서 서버에 요청을 보냄
- 응답 대기: iOS 운영체제가 백그라운드에서 작업 처리
- 응답 헤더 수신: 서버로부터 첫 응답이 오면 적절한 콜백 호출
- 데이터 수신: 서버로부터 데이터가 오면 적절한 콜백 호출 (여러 번 가능)
- 작업 완료: 모든 데이터가 도착하거나 오류가 발생하면 완료 콜백 호출
클로저 vs 델리게이트의 차이점
클로저 방식은 8번에서 모든 걸 한 번에 처리해요:
URLSession.shared.dataTask(with: url) { data, response, error in
// 완성된 data가 한 번에 들어옴
print("받은 데이터: \(data?.count ?? 0)바이트")
}.resume()
하지만 델리게이트 방식은 6-8번 과정을 세분화해서 각각 다른 메서드로 처리할 수 있어요:
let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil)
let task = session.dataTask(with: url) // 클로저 없음!
task.resume()
델리게이트 메서드의 실행 순서
- didReceiveResponse: 서버로부터 첫 번째 응답 헤더가 도착했을 때 호출됩니다.
- didReceiveData: 서버로부터 데이터 패킷을 수신할 때마다 반복적으로 호출됩니다.
그렇기에 큰 데이터를 받을 때 이 메서드는 여러 번 호출되며, 각 호출마다 일부 데이터가 전달됩니다.
개발자는 이 데이터를 누적하여 최종 데이터를 완성해야 해요. - willCacheResponse: 데이터를 캐시할지 여부를 결정할 수 있는 기회를 제공합니다.
⚠️🚨 에러 핸들링은 어떻게 할까요?
URLSession에서 발생하는 에러는 크게 시스템 레벨 에러와 HTTP 레벨 에러로 나눌 수 있어요:
extension URLSession {
func safeDataTask(
with request: URLRequest,
completion: @escaping (Result<Data, NetworkError>) -> Void
) -> URLSessionDataTask {
return dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
// 1️⃣ 시스템 에러 체크
if let error = error {
completion(.failure(NetworkError.from(error)))
return
}
// 2️⃣ HTTP 응답 검증
guard let httpResponse = response as? HTTPURLResponse else {
completion(.failure(.invalidResponse))
return
}
// 3️⃣ 상태코드 체크
guard 200...299 ~= httpResponse.statusCode else {
completion(.failure(.httpError(httpResponse.statusCode)))
return
}
// 4️⃣ 데이터 검증
guard let data = data else {
completion(.failure(.noData))
return
}
// 5️⃣ 성공! 🎉
completion(.success(data))
}
}
}
}
이렇게 하면 에러 처리가 체계적이고 놓치는 케이스 없이 안전하게 네트워크 통신을 할 수 있어요!
Background Transfer
Background Transfer는 앱이 완전히 종료되어도 다운로드가 계속 진행되는 기능이에요 ✨
Session Daemon
이 녀석은 운영체제(Core OS) 레벨에서 돌아가는 별도의 프로세스라서 앱과 완전히 독립적으로 동작해요.
- 작업 위임: URLSession이 백그라운드 세션을 생성하면 모든 작업 정보가 Session Daemon으로 넘어가요
- 독립 실행: 앱이 종료되어도 데몬은 계속 살아있으면서 서버와 통신을 유지해요
- 앱 부활: 작업이 완료되면 iOS가 앱을 다시 켜서 결과를 알려줘요
백그라운드 세션 구현하기
1. 백그라운드 세션 생성
let configuration = URLSessionConfiguration.background(withIdentifier: "com.myapp.background")
let session = URLSession(configuration: configuration, delegate: self, delegateQueue: nil)
중요한 점: withIdentifier는 고유해야 해요! 같은 식별자로 여러 세션을 만들면 문제가 생겨요.
2. AppDelegate에서 재연결 처리
앱이 죽었다가 다시 살아날 때를 대비해서 AppDelegate에 이 메서드를 구현해야 해요:
func application(_ application: UIApplication,
handleEventsForBackgroundURLSession identifier: String,
completionHandler: @escaping () -> Void) {
print("백그라운드 작업 완료됨! 식별자: \(identifier)")
// 완료 핸들러를 저장해둬야 함
backgroundCompletionHandler = completionHandler
}
3. 델리게이트에서 완료 처리
func urlSession(_ session: URLSession, task: URLSessionTask,
didCompleteWithError error: Error?) {
// 작업 완료 처리
// 저장해둔 completionHandler 호출 (필수!)
DispatchQueue.main.async {
self.backgroundCompletionHandler?()
self.backgroundCompletionHandler = nil
}
}
이 completionHandler를 호출하지 않으면 시스템이 "앱이 아직 작업 중이구나"라고 생각해서 앱을 다시 잠재우지 않아요. 그러면 배터리가 계속 소모되겠죠? 😱
Background Session의 제약사항
하지만 모든 게 가능한 건 아니에요. Apple이 몇 가지 제약을 걸어뒀거든요:
- HTTP/HTTPS만 지원: FTP나 커스텀 프로토콜은 안 돼요
- 파일 기반 업로드만: Data를 직접 업로드하는 스트림 방식은 불가능해요
- 리다이렉트 제한: 너무 많은 리다이렉트는 실패할 수 있어요
- 델리게이트 필수: 백그라운드에서는 클로저 방식을 쓸 수 없어요
너무 생각없이 사용만 했던 URLSession에게..
안에 기능들이 뭐가 있고 어떻게 동작하는지도 모르고 그저 URLSession쓰다가 어? Alamofire? 어? Moya 모야모야 하다가 써드파티를 쓴 제 자신이 부끄러워지네요
사실 알고보면 써드파티라이브러리들도 전부 Session 기반이고 그 내부의 URLProtocol을 상속받아 하는 객체를 완성한게 전부일텐데..
제 글을 읽고 다들 URLSession에 대해 하나라도 얻어가는게 있었으면 좋겠습니다:) 👍
'iOS' 카테고리의 다른 글
URLSession 한 줄 뒤에 숨겨진 것들: DNS부터 전자기파까지 (2) | 2025.10.11 |
---|---|
HTTP 캐싱(Etag & max-age) 그리고 iOS에서는? (0) | 2025.10.01 |
스크롤뷰는 어떻게 스크롤될까? 🤔 UIView 렌더링부터 시작하는 완벽 분석 (0) | 2025.09.26 |
데이터 보관은 어떻게이루어질까(직렬화: NSCoding부터 Codable까지) (0) | 2025.09.09 |
객체 간 통신(delegate, Closure, NotificationCenter, KVO) (0) | 2025.09.04 |