(검수중입니다.)
https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md
Session
RequestInterceptor
EventMonitor
sAlamofire는 URLSession과 Foundation의 URL Loading System을 기반으로 합니다. 이 프레임워크를 최대한 활용하려면 기본 네트워킹 스택의 개념과 기능에 익숙해지는 것을 권합니다.
읽으시기를 권장하는 글은 아래와 같습니다.
URLSession
Class ReferenceURLCache
Class ReferenceURLAuthenticationChallenge
Class Reference첫 번째 글은 이전에 번역한 적이 있어 아래 링크를 남기겠습니다.
https://velog.io/@panther222128/URL-Loading-System
Session
Alamofire의 Session
은 대략적으로 URLSession의 인스턴스와 동일합니다. URLSession처럼 Alamofire는 다양한 Request
를 만들 수 있는 API를 제공하고, 이는 다른 URLSessionTask 서브클래스를 캡슐화합니다. 또한 인스턴스에 의해 제공되는 모든 Request
에 적용되는 다양한 configuration을 캡슐화합니다.
Session
은 default 싱글턴 인스턴스를 제공하며, 이 인스턴스는 열거형에서 AF를 통해 가장 상위 레벨 API를 작동하도록 합니다. 아래 두 가지 내용과 동일합니다.
AF.request("https://httpbin.org/get")
let session = Session.default
session.request("https://httpbin.org/get")
대부분의 앱이 다양한 방식으로 Session
인스턴스를 customize한 형태로 동작할 수 있기를 요구합니다. 아래와 같은 convenience initializer
를 사용하는 것이 가장 쉬운 방법이며, 앱에서 사용되는 싱글톤에 결과를 저장합니다.
public convenience init(configuration: URLSessionConfiguration = URLSessionConfiguration.af.default,
delegate: SessionDelegate = SessionDelegate(),
rootQueue: DispatchQueue = DispatchQueue(label: "org.alamofire.session.rootQueue"),
startRequestsImmediately: Bool = true,
requestQueue: DispatchQueue? = nil,
serializationQueue: DispatchQueue? = nil,
interceptor: RequestInterceptor? = nil,
serverTrustManager: ServerTrustManager? = nil,
redirectHandler: RedirectHandler? = nil,
cachedResponseHandler: CachedResponseHandler? = nil,
eventMonitors: [EventMonitor] = [])
이 initializer는 기본적인 Session
움직임에 대한 모든 사항을 customize할 수 있도록 합니다.
URLSession의 움직임을 customize하려면 customize된 형태의 URLSessionConfiguration 인스턴스가 제공되어야 합니다. URLSessionConfiguration.af.default 인스턴스로부터 출발하는 것을 권장하며, Alamofire가 제공하는 Accept-Encoding, Accept-Language, User-Agent 헤더를 추가하지만 모든 URLSessionConfiguration을 사용할 수 있습니다.
let configuration = URLSessionConfiguration.af.default
configuration.allowsCellularAccess = false
let session = Session(configuration: configuration)
Authorization 혹은 Content-Type 헤더는 URLSessionConfiguration에 위치시키는 것이 좋지 않습니다. 대신 제공되는 헤더 API를 사용해
Request
에 추가하는 것이 좋습니다. ParameterEncoder 혹은 RequestAdapter를 사용하면서 그렇게 할 수 있을 것입니다.
애플이 아래 링크에서 언급한 것처럼, 인스턴스가 URLSession에 추가된 후 URLSessionConfiguration 속성을 변경시키는 것(Alamofire의 경우
Session
의 initialize가 사용된 후에 속성 변경)은 효과가 없습니다.
https://developer.apple.com/documentation/foundation/urlsessionconfiguration
SessionDelegate
SessionDelegate
인스턴스는 URLSessionDelegate와 관련 프로토콜 콜백의 모든 처리를 캡슐화합니다. SessionDelegate
는 Alamofire에서 제공하는 모든 Request
에 대해 SessionStateDelegate
의 역할을 합니다. 이를 통해 간접적으로 상태를 가져올 수 있도록 허용하며, 요청을 생성한 Session
인스턴스로부터 가져옵니다. SessionDelegate
는 특정 FileManager
인스턴스를 통해 customize될 수 있으며, UploadRequest
에 의해 파일 업로드에 접근하거나 DownloadRequest
에 의해 파일을 다운로드할 수 있도록 파일에 접근하는 것과 같은 모든 디스크 접근을 위해 사용하게 될 것입니다.
let delegate = SessionDelegate(fileManager: .default)
startRequestsImmediately
default로 Session은 적어도 하나 이상의 리스폰스 핸들러가 추가되자마자 Request
에 대한 resume()
을 호출할 것입니다. startRequestsImmediately
를 false
로 세팅하면 모든 Request는 resume()
을 직접 호출해야 합니다..
let session = Session(startRequestsImmediately: false)
Session
’s DispatchQueues
default로 Session
인스턴스는 비동기 동작에 관해 하나의 DispatchQueue
를 사용합니다. 이는 underlyingQueue
를 포함하는데, 그것은URLSession
의 delegate
, OperationQueue
입니다. 모든 URLRequest
생성, 리스폰스 동기화 작업, 내부의 Session
과 Request 상태 변경에서 underlyingQueue
를 포함합니다. 만약 URLRequest
생성이나 리스폰스 동기화 근처에 성능 분석상 병목현상이 나타난다면, 각각의 작동 영역에 있어 Session
은 분리된 DispatchQueue
를 제공받도록 할 수 있습니다.
let rootQueue = DispatchQueue(label: "com.app.session.rootQueue")
let requestQueue = DispatchQueue(label: "com.app.session.requestQueue")
let serializationQueue = DispatchQueue(label: "com.app.session.serializationQueue")
let session = Session(rootQueue: rootQueue,
requestQueue: requestQueue,
serializationQueue: serializationQueue)
모든 커스텀 rootQueue
는 반드시 하나의 serial queue여야 합니다. requestQueue
와 serializationQueue
는 serial 혹은 paralell일 수 있습니다. 성능 분석상 작업이 느려지지 않는 한 serial queue가 default이길 권장합니다. 이 경우 queue를 parallel하게 하면 전반적 성능에 도움이 될 수 있습니다.
Alamofire의 RequestInterceptor
프로토콜(RequestAdapter & RequestRetrier
)은 중요하고 강력한 Request adaptation과 retry 기능을 제공합니다. Session
과 Request
레벨에서 적용될 수 있습니다. RequestInterceptor
자체와 Alamofire로 구현하는 다양한 방식은 RetryPolicy
와 같은 아래 부분에서 살펴보시기 바랍니다.
let policy = RetryPolicy()
let session = Session(interceptor: policy)
ServerTrustManager
Alamofire의 ServerTrustManager
클래스는 도메인과 ServerTrustEvaluating
타입을 따르는 인스턴스의 맵핑을 캡슐화합니다. 이를 통해 Session
의 TLS 보안을 다루는 것을 customize할 수 있도록 합니다. 이는 인증서 사용, 키 고정, 인증서 해지 확인의 사용을 가능하게 합니다. 관련해 더 많은 것을 알고 싶으시다면 아래 ServerTrustManager
와 ServerTrustEvaluating
부분을 살펴보시기 바랍니다. ServerTrustManager
를 초기화하는 것은 도메인과 수행하게 될 평가 타입 사이 맵핑을 나타냄으로써 쉽게 할 수 있습니다.
let manager = ServerTrustManager(evaluators: ["httpbin.org": PinnedCertificatesTrustEvaluator()])
let session = Session(serverTrustManager: manager)
더 자세한 부분은 아래 내용에서 살펴보시기 바랍니다.
RedirectHandler
Alamofire의 RedirectHandler
프로토콜은 HTTP redirect 리스폰스를 다루는 것을 customize할 수 있도록 도와줍니다. Session
과 Request
레벨 모두에서 적용될 수 있습니다. Alamofire는 RedirectHandler
를 준수하는 Redirector
타입을 갖고 있으며, redirect에 있어 간단한 컨트롤을 제공합니다. RedirectHandler
에 대해 더 알고 싶으시다면 아래 내용을 참고하시기 바랍니다.
let redirector = Redirector(behavior: .follow)
let session = Session(redirectHandler: redirector)
CachedResponseHandler
Alamofire의 CachedResponseHandler
프로토콜은 리스폰스의 caching을 customize하며, 이는 Session
과 Request
레벨 모두에서 적용될 수 있습니다. Alamofire는 ResponseCacher
타입을 갖고 있고 이 타입은 CachedResponseHandler
를 준수합니다. 이를 통해 리스폰스 caching의 간단한 컨트롤을 제공합니다. 아래 내용에서 자세한 내용을 보실 수 있습니다.
let cacher = ResponseCacher(behavior: .cache)
let session = Session(cachedResponseHandler: cacher)
EventMonitor
sAlamofire는 내부의 이벤트에 관한 강력한 인사이트를 주는 EventMonitor
프로토콜을 갖고 있습니다. EventMonitor
는 로깅과 기타 이벤트 기반의 기능을 제공하는 데 사용됩니다. Session
은 초기화될 때 EventMonitor
인스턴스를 따르는 배열을 허용합니다.
let monitor = ClosureEventMonitor()
monitor.requestDidCompleteTaskWithError = { (request, task, error) in
debugPrint(request)
}
let session = Session(eventMonitors: [monitor])
사용은 드물겠지만 Session
은 active 상태의 모든 Request
에 대해 제어할 수 있는 withAllRequests
메소드를 제공합니다. Session
의 rootQueue
에서 작동하게 될 것이기 때문에 빠르게 유지하는 것이 중요합니다. 만약 동작에 시간이 걸린다면 Request
의 Set
을 처리하기 위해 분리된 queue를 생성하는 것이 필요합니다.
let session = ... // Some Session.
session.withAllRequests { requests in
requests.forEach { $0.suspend() }
}
추가적으로 Session
은 모든 Request
를 취소하기 위한 convenience를 제공하고 있고, 완료될 때 completion handler를 호출합니다.
let session = ... // Some Session.
session.cancelAllRequests(completingOn: .main) { // completingOn uses .main by default.
print("Cancelled all requests.")
}
Note: 이 움직은들은 비동기적으로 작동합니다. 그렇기 때문에 request는 생성되거나 실행될 때 완료될 수 있습니다. 그래서 특정
Request
의 set에 대해 작업이 수행될 것이라고 생각하면 안 됩니다.
URLSession
s앞서 설명한 convenience initializer
에 덧붙여, Session
은 URLSession으로부터 직접적으로 초기화될 수 있습니다. 그러나 이 initializer를 사용하기 위해서 몇 가지 요구사항이 존재합니다. 그렇기 때문에 'convenience initializer'를 권장합니다. 아래를 포함합니다.
Alamofire는 background 사용에 대해 URLSession으로부터 configure된 것은 지원하지 않습니다. Session
이 초기화될 때 런타임 에러가 발생할 것입니다.
SessionDelegate
인스턴스가 생성되어야 하고 URLSession의 delegate
로써 사용되어야 합니다. 또한, Session
initializer가 전달됩니다.
하나의 custom OperationQueue
가 URLSession의 delegateQueue
에 전달되어야 합니다. 이 queue는 serial queue여야 하며, 이 queue는 백업 DispatchQueue
를 가져야만 하고 이 DispatchQueue
가 Session
의 rootQueue
로써 전달되어야 합니다.
let rootQueue = DispatchQueue(label: "org.alamofire.customQueue")
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.underlyingQueue = rootQueue
let delegate = SessionDelegate()
let configuration = URLSessionConfiguration.af.default
let urlSession = URLSession(configuration: configuration,
delegate: delegate,
delegateQueue: queue)
let session = Session(session: urlSession, delegate: delegate, rootQueue: rootQueue)
Alamofire에서 기능하는 각각의 Request
는 특정 클래스에 의해 캡슐화되며,DataRequest
, UploadRequest
, DownloadRequest
입니다. 이와 같은 클래스는 각각의 Request
타입에 맞춰 고유한 기능을 캡슐화합니다. 그런데 DataRequest
, DownloadRequest
는 공통 상위 클래스로부터 상속됩니다.(UploadRequest
는 DataRequest
로부터 상속) Request
인스턴스는 직접적으로 생성되지 않고, 대신 Session
인스턴스로부터 다양한 Request
메소드 중 하나를 통해 내보내집니다.
초기 파라미터 혹은 URLRequestConvertible
와 함께 Request
서브클래스가 한 번 생성되면, Alamofire의 Request
파이프라인을 구성하는 일련의 단계를 거치게 됩니다. Request
가 성공적이면 아래 내용을 포함합니다.
HTTP 메소드, 헤더, 기타 파라미터와 같은 초기 파라미터는 내부의 URLRequestConvertible
값에 캡슐화됩니다. 만약 URLRequestConvertible
값이 직접적으로 전달된다면, 그 값은 변경되지 않고 사용됩니다.
asURLRequest()
는 URLRequestConvertible
값에서 호출되며, 첫 번째 URLRequest
값을 생성합니다. 이 값은 Request
에 전달되고 requests
에 저장됩니다. 만약 URLRequestConvertible
값이 Session
메소드에서 전달된 파라미터로부터 생성되었다면, URLRequest
가 생성될 때 모든 RequestModifier
가 호출됩니다.
만약 Session
혹은 Request
, RequestAdpter
혹은 RequestInterceptor
가 하나라도 있다면, 이전에 생성된 URLRequest
를 사용하면서 호출될 것입니다. 적용된 URLRequest
는 Request
에 전달되고 request
에 저장됩니다.
URLRequest
에 기반한 네트워크 request를 수행하려면 URLSessionTask
를 생성해야 합니다. 이를 위해 Session
이 Request
를 호출합니다.
URLSessionTask
가 완료되고 URLSessionTaskMetrics
가 모이면, Request
는 Validator
를 수행합니다.
Request는 모든 리스폰스 핸들러를 수행합니다. 추가된 responseDecodable
과 같은 것이 있습니다.
단계 중에서 실패가 나타날 수 있으며, Error
값을 생성 혹은 받는 것을 통해 알 수 있고 이 Error
는 연관이 있는 Request
에 전달됩니다. 예를 들어 1단계에서 4단계까지를 제외하고 모든 단계는 Error
를 생성할 수 있으며, 리스폰스 핸들러에 전달되거나 retry를 해볼 수 있을 것입니다. 아래에서 Request
파이프라인을 통해서 실패할 수 있거나 혹은 실패하지 않는 몇 가지 예를 살펴볼 수 있습니다.
파라미터 캡슐화가 실패할 수 있습니다.
asURLRequest()
가 호출될 때 URLRequestConvertible
값이 에러를 생성할 수 있습니다. 이는 다양한 URLRequest
프로퍼티에 대한 초기 검증이나 파라미터 인코딩 실패를 가능하게 합니다.
RequestAdapter
는 적용되는 동안 실패할 수 있고, 아마도 인증 토큰을 잃어버렸을 때 실패가 발생할 것입니다.
URLSessionTask
생성이 실패할 수 있습니다.
URLSessionTask
는 다양한 이유로 인해서 오류와 함께 완료될 수 있으며, 이는 네트워크 가용과 취소를 포함합니다. 이 Error
값들은 Request
로 되돌아갑니다.
리스폰스 핸들러는 Error
를 생성할 수 있으며, 보통 타당하지 않은 리스폰스나 다른 파싱 에러에 기인합니다.
에러가 Request
에 전달될 때, Request
는 Session
혹은 Request
와 연관이 있는 RequestRetrier
를 시도할 수 있습니다. 만약 RequestRetrier
가 Request
를 retry하기를 선택한다면, 완료된 파이프라인이 다시 작동합니다. RequestRetrier
는 Error
를 생성할 수도 있으며 retry를 시도하지 않습니다.
Request
Request
가 특정 request의 유형을 캡슐화하지 않을지라도, Alamofire가 수행하는 모든 request에 대한 공통적인 상태와 기능을 갖고 있을 것입니다. 아래와 같은 내용을 포함합니다.
모든 Request
유형은 상태 개념을 포함하고 있으며, Request
의 lifetime 안에서 주요 이벤트를 나타나도록 할 것입니다.
public enum State {
case initialized
case resumed
case suspended
case cancelled
case finished
}
Request
는 생성 후 .initialized
상태에서 시작합니다. Request
는 멈춰지거나 재개할 수 있고 취소될 수도 있으며, 걸맞는 lifetime 메소드를 호출하는 것을 통해 이뤄집니다.
resume()
메소드는 Request
의 네트워크 트래픽을 재개하거나 시작합니다. 만약 startRequestsImmediately
가 true
이면, 리스폰스 핸들러가 Request
에 추가되었을 때 자동으로 호출됩니다.
suspend()
메소드는 Request
의 네트워크 트래픽을 suspend하거나 일시정지합니다. 이 상태의 Request
는 재개될 수 있으나 오직 DownloadRequests
만 데이터 전달을 지속할 수 있을 것입니다. 다른 Request
는 다시 시작됩니다.
cancel()
메소드는 Request
를 취소합니다. 이 상태에 있으면 Request
는 재개되거나 중단될 수 없습니다. cancel()
메소드가 호출될 때 Request
의 error
프로퍼티가 AFError.explicitlyCancelled
인스턴스와 함께 설정될 것입니다. 만약 Request
가 재개되고 이후 취소되지 않는다면, 모든 리스폰스 validator와 리스폰스 serializer가 실행됐을 때 .finished
상태에 도달할 것입니다. 그러나 .finished
상태에 Request
가 도달했을 때 추가적인 리스폰스 serializer가 Request
에 추가되면, .resumed
상태로 전환되고 네트워크 request를 다시 수행할 것입니다.
request의 progress를 추적하기 위해 Request
는 uploadProgress
와 downloadProgress
속성을 제공하고, 클로저 기반의 uploadProgress
와 downloadProgress
메소드를 제공합니다. 모든 클로저 기반의 Request
API처럼 progress API 역시 다른 메소드와 함께 Request
에서 연결될 수 있습니다. 또한, 다른 여러 가지 클로저 기반 API처럼 progress API는 request에 추가되어야 하며, 추가될 때는 responseDecodable
과 같은 어떠한 리스폰스 핸들러가 추가되기 전에 추가되어야 합니다.
AF.request(...)
.uploadProgress { progress in
print(progress)
}
.downloadProgress { progress in
print(progress)
}
.responseDecodable(of: SomeType.self) { response in
debugPrint(response)
}
중요한 점은 모든 Request
서브클래스가 progress를 정확하게 보고하지 않는다는 점입니다. 혹은 그렇게 하기 위한 다른 dependency를 갖도록 해야 할 것입니다.
UploadRequest
에 제공되는 Data
객체의 길이에 의해UploadRequest
의 upload body로써 제공된 디스크의 파일 길에 의해Content-Length
헤더의 값에 의해(만약 직접적으로 설정했다면)Content-Length
헤더를 포함해야만 함안타깝게도 URLSession
으로부터 progress 보고를 받으려고 할 때, 정확한 보고를 방해할 수 있는 요구사항들이 있으며 이 요구사항들은 문서화되어있지 않습니다.
Alamofire의 RedirectHandler
프로토콜은 Request
에 대한 redirect handling의 컨트롤과 customization을 제공합니다. Session
별 RedirectHandler
에 추가하여 각각의 Request
는 스스로 보유할 수 있는 RedirectHandler
가 주어질 수 있으며, Session
에서 제공하는 모든 것을 override합니다.
let redirector = Redirector(behavior: .follow)
AF.request(...)
.redirect(using: redirector)
.responseDecodable(of: SomeType.self) { response in
debugPrint(response)
}
Note: 하나의
Request
에는 오직 하나의RedirectHandler
만 설정할 수 있습니다. 하나 이상을 주려고 하려면 runtime exception 결과가 나타날 것입니다.
Alamofire의 CachedResponseHandler
프로토콜은 리스폰스 캐싱에 대한 컨트롤과 customization을 제공합니다. Session
별 CachedResponseHandler
에 추가하여 각각의 Request
는 스스로 보유할 수 있는 CachedResponseHandler
가 주어질 수 있으며, Session
에서 제공하는 모든 것을 override합니다.
let cacher = ResponseCacher(behavior: .cache)
AF.request(...)
.cacheResponse(using: cacher)
.responseDecodable(of: SomeType.self) { response in
debugPrint(response)
}
Note: 하나의
CachedResponseHandler
는 하나의Request
에만 설정될 수 있습니다. 하나 이상을 주려고 하려면 runtime exception 결과가 나타날 것입니다.
URLSession
에서 제공하는 자동 credential handling의 이점을 이용하기 위해 Alamofire는 Request
별 API를 제공하며, 이 API는 URLCredential
인스턴스를 자동으로 추가할 수 있도록 합니다.이와 같은 것들은 사용자 이름과 패스워드를 사용하는 HTTP 인증을 위한 convenience API를 제공하고, 모든 URLCredential
인스턴스 역시 제공합니다.
HTTP 인증에 자동으로 응답하는 credential을 추가하는 것은 간다합니다.
AF.request(...)
.authenticate(username: "user@example.domain", password: "password")
.responseDecodable(of: SomeType.self) { response in
debugPrint(response)
}
Note: 이 메커니즘은 오직 HTTP 인증 프롬프트만을 지원합니다. 만약 하나의 request가 모든 request에 대해
Autentication
헤더를 요청하려고 한다면, 직접적으로 제공받아야 하며Request
의 일부분으로써 제공받거나RequestInterceptor
를 통해 제공받아야 합니다.
원시 URLCredential
추가 역시 간단합니다.
let credential = URLCredential(...)
AF.request(...)
.authenticate(using: credential)
.responseDecodable(of: SomeType.self) { response in
debugPrint(response)
}
Alamofire는 Request
의 lifetime 동안 다양한 underlying 값을 생성합니다. 대부분 내부에 있는 구현에 대한 세부 정보들이며, URLRequest
와 URLSessionTask
는 다른 API와 직접적으로 상호작용할 수 있는 상태가 될 것입니다.
Request
’s URLRequest
sRequest
로 발행되는 각각의 네트워크 request는 URLRequest
값에서 캡슐화되며, 이 값은 하나의 Session
request 메소드에 전달된 다양한 파라미터로부터 생성됩니다. Request
는 스스로 갖고 있는 requests
배열 속성에 URLRequest
의 사본을 보관하게 될 것입니다. 이와 같은 값들은 보냈던 파라미터로부터 생성된 초기 URLRequest
를 포함하게 될 것이며, RequestInterceptor
에 의해 생성된 URLRequest
역시 포함하게 될 것입니다. 그러나 이 배열은 Request
를 대신해서 발행된 URLSessionTask
가 수행하는 URLRequest
는 포함하지 않을 것입니다. 이 값들을 파악하려면 tasks
프로퍼티가 Request
에 의해 수행되는 모든 URLSessionTasks
에 접근 권한을 줍니다.
이 값들을 축적하는 것과 더불어, 모든 Request
는 Request
에서 URLRequest
가 생성될 때마다 클로저를 호출하는 onURLRequestCreation
메소드를 갖습니다. 이 URLRequest
는 Session
의 request
메소드에 전달된 초기 파라미터의 결과이자 RequestInterceptor
에 의해 적용되는 변경내역이기도 합니다. 만약 Request
가 retry되면 여러번에 걸쳐 호출될 것이며, 한 번에 하나의 클로저가 설정될 수 있습니다. URLRequest
값은 이 클로저에서 수정될 수 없으며, URLRequest
가 발행되기 전에 수정이 필요하다면 RequestInterceptor
를 사용하거나 Alamofire에 전달되기 전에 URLRequestConvertible
프로토콜을 사용하는 request를 구성해야 합니다.
AF.request(...)
.onURLRequestCreation { request in
print(request)
}
.responseDecodable(of: SomeType.self) { response in
debugPrint(response)
}
URLSessionTask
s많은 방식으로 다양한 Request
서브클래스가 URLSessionTask
의 wrapper로써 기능하며, 다양한 유형의 task와 상호작용하기 위한 구체적인 API를 제공합니다. 이와 같은 task들은 tasks
배열 속성을 통해 Request
인스턴스에서 보여지도록 만들 수 있습니다. 이는 Request
에서 생성된 초기 task를 포함하며, retry 하나당 retry 프로세스의 부분으로써 생성된 모든 후속 task를 포함합니다.
이 값들을 축적시키는 것과 더불어 모든 Request
는 Request
에서 생성되는 URLSessionTask
가 생성될 때마다 클로저를 호출하는 onURLSessionTaskCreation
메소드를 갖습니다. 이 클로저는 Request
가 retry되면 여러번에 걸쳐 호출될 것이며, 한 번에 오직 하나의 클로저만 설정될 수 있습니다. 주어진 URLSessionTask
는 절대로 task
의 lifetime과 상호작용하도록 사용되면 안됩니다. task
의 lifetime은 Request
자체에 의해서만 마무리가 되어야 하는 것입니다. 대신 이 메소드를 사용함으로써 다른 API에 활성화된 task
를 전달할 수 있습니다. 대표적으로 NSFileProvider
가 있습니다.
AF.request(...)
.onURLSessionTaskCreation { task in
print(task)
}
.responseDecodable(of: SomeType.self) { response in
debugPrint(response)
}
각각의 Request
는 아마 request가 완료되었을 때 이용 가능한 HTTPURLResponse
값을 갖게 될 것입니다. 이 값은 request가 취소되지 않았었고 네트워크 request에 실패하지 않았을 때에만 이용이 가능합니다. 추가적으로 만약 request가 retry 되면, 마지막 리스폰스만 이용이 가능합니다. 중간 리스폰스는 tasks
프로퍼티에서 URLSessionTask
로부터 파생될 수 있습니다.
URLSessionTaskMetrics
Alamofire는 Request
에서 수행되는 모든 URLSessionTask
의 URLSessionTaskMetrics
값을 수집합니다. 이 값들은 metrics
프로퍼티에서 이용 가능하며, 각각의 값은 같은 인덱스에 있는 tasks
의 URLSessionTask
에 해당합니다.
URLSessionTaskMetrics
는 DataResponse
와 같은 Alamofire의 다양한 리스폰스 타입들에서 이용 가능하도록 만들어져 있습니다.
AF.request(...)
.responseDecodable(of: SomeType.self) { response in {
print(response.metrics)
}
FB7624529
때문에 7 아래 버전의 watchOS에서URLSessionTaskMetrics
의 수집은 불가능한 상태입니다.
DataRequest
DataRequest
는 메모리에 저장된 Data
에 서버 리스폰스를 다운로드하는 URLSessionDataTask
를 캡슐화하는 Request
의 서브클래스입니다. 그러므로 극도로 거대한 다운로드는 시스템 성능에 부정적인 영향을 줄 수 있다는 것을 알아야 합니다. 그러한 다운로드는 DownloadRequest
를 사용함으로써 디스크에 데이터를 저장하길 권장합니다.
DataRequest
는 Request
에서 제공하는 프로퍼티 외에 몇 가지 프로퍼티를 가지고 있습니다. 서버 리스폰스로부터 Data
를 축적된 형태의 data
를 포함합니다. 그리고 인스턴스 생성 시 오리지널 파라미터를 포함하고 있는 상태인 DataRequest
가 생성된 URLRequestConvertible
도 포함하고 있습니다.
DataRequest
는 리스폰스에 대한 검증을 default로 갖고 있지 않습니다. 대신 여러 가지 프로퍼티를 검증하기 위해서 request에 validate()
가 추가되어야 합니다.
public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> Result<Void, Error>
default에서 validate()
를 추가하는 것은 리스폰스 상태 코드가 200..<300
범위 안에 있는지 확인하고, 리스폰스의 Content-Type
이 request의 Accept
값인지도 확인합니다. Validation
클로저를 통해 검증을 costomize할 수도 있습니다.
AF.request(...)
.validate { request, response, data in
...
}
DataStreamRequest
DataStreamRequest
는 Request
의 서브클래스이며, URLSessionDataTask
를 캡슐화하고 HTTP 연결로부터 시간의 흐름에 따라 Data
를 스트리밍합니다.
DataStreamRequest
는 추가적인 public 상태를 포함하지 않습니다.
DataStreamRequest
는 default에서 리스폰스를 검증하지 않습니다. 대신 validate()
추가를 통해 여러 가지 프로퍼티에 대한 검증을 할 수 있습니다.
public typealias Validation = (_ request: URLRequest?, _ response: HTTPURLResponse) -> Result<Void, Error>
default에서 validate()
를 추가하는 것은 리스폰스 상태 코드가 200..<300
범위 안에 있는지 확인하고, 리스폰스의 Content-Type
이 request의 Accept
값인지도 확인합니다. Validation
클로저를 통해 검증을 costomize할 수도 있습니다.
AF.request(...)
.validate { request, response in
...
}
UploadRequest
UploadRequest
는 DataRequest
의 서브클래스이며, URLSessionUploadTask
를 캡슐화하고 Data
값, 디스크의 파일을 업로드하거나 원격 서버에 InputStream
을 업로드합니다.
UploadRequest
는 DataRequest
에서 제공하는 프로퍼티 외에 몇 가지 프로퍼티를 갖고 있습니다. 파일 업로드 시 디스크 접근과 관련해 customize하기 위해 사용되는 FileManager
인스턴스를 포함하고 있고, request를 설명하는 데 사용되는URLRequestConvertible
값과 업로드가 수행될 때 유형을 결정할 수 있는 Uploadable
값도 갖고 있습니다.
DownloadRequest
DownloadRequest
는 Request
의 구체적인 서브클래스이며, 디스크에 리스폰스 Data
를 다운로드를 진행하는 URLSessionDownloadTask
를 캡슐화합니다.
DownloadRequest
는 Request
에서 제공하는 프로퍼티 외에 몇 가지 프로퍼티를 갖고 있습니다. Data
가 제공되던 중 취소된 이후 다운로드를 재개하는 데 사용되는 파라미터인 resumeData
를 포함하고 있고, 다운로드 완료가 되었을 때 어디에서 다운로드된 파일을 이용할 수 있는지 구체화해주는 URL
인 fileURL
도 포함하고 있습니다.
Request
가 제공하는 cancel()
메소드와 더불어 DownloadRequest
는 cancel(producingResumeData shouldProduceResumeData: Bool)
를 제공하며, 이 메소드는 가능한 경우 선택적으로 resumeData
프로퍼티를 생성할 수 있습니다. 그리고 cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void)
메소드는 전달된 클로저에 생성된 resume 데이터를 재공합니다.
AF.download(...)
.cancel { resumeData in
...
}
DownloadRequest
는 DataRequest
와 UploadRequest
에 비해 조금 다른 버전의 검증을 지원합니다. 이는 데이터가 디스크에 다운로드된다는 사실때문입니다.
public typealias Validation = (_ request: URLRequest?, _ response: HTTPURLResponse, _ fileURL: URL?)
다운로드된 Data
에 직접 접근하는 것 대신 주어진 fileURL
을 사용해 접근되어야 합니다. 그렇게 하지 않으면 DownloadRequest
검증 기능은 DataRequest
의 검증 기능과 동일하게 될 것입니다.
RequestInterceptor
Alamofire의 RequestInterceptor
프로토콜(RequestAdapter
와 RequestRetrier
프로토콜로 구성된)은 강력한 세션 기능과 Request
기능을 제공합니다. 이들은 모든 Request
에 대해 공통 헤더가 추가되는 인증 시스템을 갖추고 있고, Request
는 인증이 만료되면 retry합니다. 추가적으로 Alamofire는 자체에 내장된 RetryPolicy
타입을 포함하고 있습니다. 이 타입은 request들이 네트워크 에러로 인해 실패했을 때 쉽게 retry할 수 있도록 지원합니다.
RequestAdapter
Alamofire의 RequestAdapter
프로토콜은 각각의 URLRequest
가 네트워크에 보내지기 전에 Session
에 의해 검사되고 변형되도록 만들어줍니다. adapter의 흔한 용도는 특정 유형의 인증 뒤로 Authorization
헤더를 request에 추가하는 것입니다.
RequestAdapter
프로토콜은 하나의 요구사항을 갖습니다.
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void)
파라미터는 아래를 포함합니다.
urlRequest
: 초기에 생성된 URLRequest
는 Request
를 생성하기 위해 사용되었던 파라미터 혹은 URLRequestConvertible
값으로부터 생성된 것입니다.
session
: Session
adapter가 호출되는 요청을 생선한 Session
입니다.
completion
: adapter가 마무리되었다는 것을 나타내기 위해 반드시 호출되어야 하는 비동기 컴플리션 핸들러입니다. 비동기라는 특성을 통해 Request
가 네트워크에 전달되기 전에, RequestAdapter
는 네트워크나 디스크로부터 비동기로 리소스에 접근할 수 있습니다. completion
클로저로부터 제공된Result
는 URLRequest
값이 수정된 형태로 .success
값을 반환하거나 Request
를 실패하는데 사용될 Error
와 함께 .failure
를 반환합니다. 예를 들어 Authorization
헤더를 추가하는 것은 URLRequest
를 수정하도록 요구하고, 그 후에 컴플리션 핸들러를 호출하는 것을 요구합니다.
let accessToken: String
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
var urlRequest = urlRequest
urlRequest.headers.add(.authorization(bearerToken: accessToken))
completion(.success(urlRequest))
}
RequestRetrier
Alamofire의 RequestRetrier
프로토콜은 Request
가 Error
를 맞이했을 때 retry가 가능하도록 만들어줍니다. 이는 request pipeline
의 단계에서 Error
를 생성할 수 있는 모든 단계를 포함합니다.
RequestRetrier
는 하나의 요구사항을 갖습니다.
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void)
파라미터는 아래 내용을 포함합니다.
request
: error를 맞이한 Request
입니다.
session
: Request
를 관리하는 session
입니다.
error
: retry 시도를 발생하게 하는 Error
입니다. 보통 AFError
입니다.
completion
: Request
가 retry되어야 하는지를 나타내기 위해 반드시 호출되어야 하는 비동기 컴플리션 핸들러입니다.
RetryResult
타입은 RequestRetrier
에서 구현된 로직의 결과를 나타냅니다. 아래와 같이 정의됩니다.
/// Outcome of determination whether retry is necessary.
public enum RetryResult {
/// Retry should be attempted immediately.
case retry
/// Retry should be attempted after the associated `TimeInterval`.
case retryWithDelay(TimeInterval)
/// Do not retry.
case doNotRetry
/// Do not retry due to the associated `Error`.
case doNotRetryWithError(Error)
}
예를 들어 request가 멱등성이라면 Alamofire의 RetryPolicy
타입은 어떤 유형의 네트워크 에러로 인해 실패한 Request
를 자동으로 retry할 것입니다.
open func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
if request.retryCount < retryLimit,
let httpMethod = request.request?.method,
retryableHTTPMethods.contains(httpMethod),
shouldRetry(response: request.response, error: error) {
let timeDelay = pow(Double(exponentialBackoffBase), Double(request.retryCount)) * exponentialBackoffScale
completion(.retryWithDelay(timeDelay))
} else {
completion(.doNotRetry)
}
}
RequestInterceptor
sAlamofire는 여러개의 RequestInterceptor
를 사용할 수 있도록 지원합니다. Session
과 Request
레벨 모두에서 그렇게 할 수 있으며, Interceptor
타입을 사용하는 것을 통해 구현할 수 있습니다. Interceptor
는 adapter와 retrier 클로저를 통해 구성될 수 있고, RequestAdapter
와 RequestRetrier
의 조합, 혹은 RequestAdapter
, RequestRetrier
, RequestInterceptor
의 배열 조합을 통해 구성될 수도 있습니다.
let adapter = // Some RequestAdapter
let retrier = // Some RequestRetrier
let interceptor = // Some RequestInterceptor
let adapterAndRetrier = Interceptor(adapter: adapter, retrier: retrier)
let composite = Interceptor(interceptors: [adapterAndRetrier, interceptor])
여러개의 RequestAdapter
를 호출할 때, Interceptor
는 각각의 RequestAdapter
를 호출할 것입니다. 만약 성공하면 RequestAdapter
의 연쇄에서 마지막 URLRequest
가 request를 수행하기 위해 사용될 것입니다. 하나가 실패하면 adaptation은 멈추고 Request
는 실패하면서 error를 반환합니다. 유사하게 여러 RequestRetrier
를 구성하는 경우 인스턴스에 추가되었던 retry들의 순서에 따라 retry가 수행됩니다. 그리고 모든 retry는 성공적으로 완료되거나 하나가 여럿 중 실패하면 error를 반환할 것입니다.
AuthenticationInterceptor
Alamofire의 AuthenticationInterceptor
클래스는 request들을 인증하는 것과 연관이 있는 queueing과 스레딩 복잡성을 다루기 위해 설계된 RequestInterceptor
입니다.
이것은 일치하는 AutehnticationCredential
의 수명주기를 관리하는 주입된 Autenticator
프로토콜을 활용합니다. 아래 내용이 어떻게 OAuthAuthenticor
클래스가 OAuthCredential
과 함께 구현되는지를 보여줍니다.
OAuthCredential
struct OAuthCredential: AuthenticationCredential {
let accessToken: String
let refreshToken: String
let userID: String
let expiration: Date
// Require refresh if within 5 minutes of expiration
var requiresRefresh: Bool { Date(timeIntervalSinceNow: 60 * 5) > expiration }
}
OAuthAuthenticator
class OAuthAuthenticator: Authenticator {
func apply(_ credential: OAuthCredential, to urlRequest: inout URLRequest) {
urlRequest.headers.add(.authorization(bearerToken: credential.accessToken))
}
func refresh(_ credential: OAuthCredential,
for session: Session,
completion: @escaping (Result<OAuthCredential, Error>) -> Void) {
// Refresh the credential using the refresh token...then call completion with the new credential.
//
// The new credential will automatically be stored within the `AuthenticationInterceptor`. Future requests will
// be authenticated using the `apply(_:to:)` method using the new credential.
}
func didRequest(_ urlRequest: URLRequest,
with response: HTTPURLResponse,
failDueToAuthenticationError error: Error) -> Bool {
// If authentication server CANNOT invalidate credentials, return `false`
return false
// If authentication server CAN invalidate credentials, then inspect the response matching against what the
// authentication server returns as an authentication failure. This is generally a 401 along with a custom
// header value.
// return response.statusCode == 401
}
func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: OAuthCredential) -> Bool {
// If authentication server CANNOT invalidate credentials, return `true`
return true
// If authentication server CAN invalidate credentials, then compare the "Authorization" header value in the
// `URLRequest` against the Bearer token generated with the access token of the `Credential`.
// let bearerToken = HTTPHeader.authorization(bearerToken: credential.accessToken).value
// return urlRequest.headers["Authorization"] == bearerToken
}
}
Usage
// Generally load from keychain if it exists
let credential = OAuthCredential(accessToken: "a0",
refreshToken: "r0",
userID: "u0",
expiration: Date(timeIntervalSinceNow: 60 * 60))
// Create the interceptor
let authenticator = OAuthAuthenticator()
let interceptor = AuthenticationInterceptor(authenticator: authenticator,
credential: credential)
// Execute requests with the interceptor
let session = Session()
let urlRequest = URLRequest(url: URL(string: "https://api.example.com/example/user")!)
session.request(urlRequest, interceptor: interceptor)
서버 및 웹 서비스와 소통할 때 보안 HTTPS 연결을 사용하는 것은 민감한 데이터를 보호하는 중요한 단계입니다. default로 Alamofire는 동일한 자동 TLS 인증서와 URLSession
으로써 인증서 체인 검증을 받습니다. 이는 인증서 체인이 유효함을 보장하면서도, man-in-the-middle(MITM) 공격 혹은 다른 잠재적 취약성을 보호하지는 않습니다. MITM 공격을 완화하려면 고객의 민감한 데이터를 다루거나 금융 정보 같은 데이터를 다루는 앱은 인증서나 공용 키 고정을 사용해야만 하며, Alamofire의 ServerTrustEvaluating
프로토콜을 통해 구현해야 합니다.
ServerTrustManager
and ServerTrustEvaluating
ServerTrustEvaluating
ServerTrustEvaluating
프로토콜은 모든 서버 신뢰 평가를 수행하는 하나의 방법을 제공합니다. 한 가지 요구사항이 있습니다.
func evaluate(_ trust: SecTrust, forHost host: String) throws
이 메소드는 URLSession
에서 받는 SecTrust
값과 호스트 String
을 제공하며, 다양한 평가를 수행할 기회를 제공합니다.
Alamofire는 신뢰 평가를 하는 주체의 여러 가지 타입을 포함하고 있으며, 평가 과정에 걸쳐 구성 가능한 컨트롤을 제공하고 있습니다. 아래와 같습니다.
DefaultTrustEvaluator
: 이 타입은 제공받은 호스트 중 어떤 것이 유효한지 제어할 수 있도록 돕는 default 서버 신뢰 평가를 사용합니다.
RevocationTrustEvaluator
: 이 타입은 취소되지 않았는지 확인하기 위해 제공받은 인증서의 상태를 확인합니다. 수반하는 네트워크 request 오버헤드 때문에 모든 request에서 수행되는 것은 아닙니다.
PinnedCertificatesTrustEvaluator
: 이 타입은 서버 신뢰에 대한 유효성을 검사하기 위해 제공받은 인증서를 사용합니다. 서버 신뢰는 하나의 고정된 인증서가 서버 인증서 중 하나와 일치한다면 유효한 것으로 여겨집니다. 이 타입은 self-signed 인증서들을 수락하기도 합니다.
PublicKeysTrustEvaluator
: 이 타입은 서버 신뢰에 대한 유효성을 검사하기 위해 제공받은 공용 키를 사용합니다. 서버 신뢰는 하나의 고정 공용 키가 서버 인증서 공용 키와 일치할 때 유효한 것으로 여겨집니다.
CompositeTrustEvaluator
: 이 타입은 ServerTrustEvaluating
값의 배열을 평가하고, 배열의 모든 것이 성공적일 때에만 성공한 것으로 평가합니다. 이 타입은 예를 들어 RevocationTrustEvaluator
와 PinnedCertificatesTrustEvaluator
와 같은 것을 조합하기 위해 사용될 수 있습니다.
DisabledTrustEvaluator
: 이 타입은 다른 타입 모두의 평가를 비활성화하기 때문에 디버그 시나리오에서만 사용되어야 합니다. 이는 모든 서버 신뢰를 항상 유효한 것으로 간주합니다. 이 타입은 절대로 production environments에서 사용하면 안 됩니다.
ServerTrustManager
는 ServerTrustEvaluating
값과 특정 호스트의 내부적 맵핑을 저장하는 역할을 합니다. 이를 통해 Alamofire가 다른 평가 타입에서도 각각의 호스트를 평가할 수 있도록 합니다.
let evaluators: [String: ServerTrustEvaluating] = [
// By default, certificates included in the app bundle are pinned automatically.
"cert.example.com": PinnedCertificatesTrustEvaluator(),
// By default, public keys from certificates included in the app bundle are used automatically.
"keys.example.com": PublicKeysTrustEvaluator(),
]
let manager = ServerTrustManager(evaluators: serverTrustPolicies)
ServerTrustManager
는 아래와 같은 움직임을 갖게 될 것입니다.
cert.example.com
은 항상 default 및 인증서 고정이 활성화된 상태에서 인증서 고정을 사용할 것입니다. 그러므로 TLS handshake가 성공적임을 수락하기 위해서 아래 기준들이 충족시길 요구합니다.keys.example.com
은 항상 default 및 인증서 고정이 활성화되어 있는 상태에서 인증서 고정을 사용할 것입니다. 그러므로 TLS handshake가 성공적임을 수락하기 위해서 아래 기준들이 충족시길 요구합니다.ServerTrustManager
가 default로 모든 호스트에 대한 평가를 요구하기 때문입니다.일치 동작에 관해 더 유연한 서버 신뢰 정책이 필요하다면, ServerTrustManager
를 서브클래싱하고 serverTrustEvaluator(forHost:)
메소드를 override 해서 고유의 custom 구현이 가능합니다.
final class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
override func serverTrustEvaluator(forHost host: String) -> ServerTrustEvaluating? {
var policy: ServerTrustPolicy?
// Implement your custom domain matching behavior...
return policy
}
}
iOS 9에서 App Transport Security (ATS)의 추가와 함께, 여러 ServerTrustEvaluating
객체를 사용하는 custom ServerTrustManager
는 효과가 없을 것입니다. 만약 CFNetwork SSLHandshake failed (-9806)
error들을 지속적으로 보게 된다면, 이 문제에 부딪힌 경우입니다. 앱의 plist에서 앱이 서버 신뢰를 평가하게끔 충분히 비활성화하도록 앱의 ATS 설정을 configure하지 않는 한, 애플의 ATS 시스템은 전체 challenge 시스템을 override합니다. 이 문제(높은 확률로 self-signed 인증서일 것)를 만나면, NSAppTransportSecurity
overrides를 Info.plist
에 추가하는 것을 통해 문제를 해결할 수 있을 것입니다. nscurl
툴의 --ats-diagnotics
옵션을 사용해 어떤 ATS override가 필요한지 보기 위한 일련의 호스트에 대한 테스트를 수행할 수 있습니다.
로컬호스트에서 작동하는 서버에 연결을 시도한다면, 그리고 self-signed 인증서들을 사용하려고 한다면, Info.plist
에 아래 내용을 추가해야 합니다.
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
Apple documentation에 따르면, 앱에서 ATS를 비활성화하는 것 없이 NSAllowsLocalNetworking
을 YES
로 설정하는 것을 통해 로컬 리소스를 로딩할 수 있습니다.
URLSession
은 URLSessionDataDelegate
와 URLSessionTaskDelegate
을 사용함을 통해 캐싱 움직임과 redirect 움직임을 customization할 수 있도록 합니다. Alamofire는 CachedResponseHandler
와 RedirectHandler
프로토콜로 customization 포인트들을 표시합니다.
CachedResponseHandler
CachedResponseHandler
프로토콜은 request를 생성 Session
과 관련이 있는 URLCache
인스턴스에서 HTTP 리스폰스를 캐싱하는 것에 대한 컨트롤이 가능하도록 합니다. 하나의 요구사항이 필요합니다.
func dataTask(_ task: URLSessionDataTask,
willCacheResponse response: CachedURLResponse,
completion: @escaping (CachedURLResponse?) -> Void)
메소드 시그니처에서 보여지는 것처럼, 이 컨트롤은 네트워크 transfer를 위해 URLSessionDataTask
를 사용하는 Request
에만 적용됩니다. 여기서 네트워크 transfer는 DataRequest
,UploadRequest
(URLSessionUploadTask
가 URLSessionDataTask
서브클래스이기 때문)를 포함합니다. 캐싱을 위해 리스폰스를 고려할 때 생각할 수 있는 조건은 여러 가지가 있습니다. 그렇기 때문에 URLSessionDataDelegate
메소드 urlSession(_:dataTask:willCacheResponse:completionHandler:)
를 다시 살펴보는 것이 중요합니다. 캐싱을 위한 리스폰스를 고려할 때, 만들 수 있는 다양한 방법들이 있습니다.
nil
CachedURLResponse
를 반환하는 것을 통해 리스폰스 캐싱을 방지할 수 있습니다.CachedURLResponse
의 storagePolicy
를 수정할 수 있습니다.URLResponse
를 직접적으로 수정할 수 있습니다.Data
를 수정할 수 있습니다.Alamofire는 ResponseCacher
타입을 포함하고 있으며, 이 타입은 CachedResponseHandler
를 준수합니다. 이 타입은 리스폰스를 캐시하지 않고 쉽게 캐시하거나 수정할 수 있습니다.
public enum Behavior {
/// Stores the cached response in the cache.
case cache
/// Prevents the cached response from being stored in the cache.
case doNotCache
/// Modifies the cached response before storing it in the cache.
case modify((URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)
}
위 내용처럼 ResponseChacer
는 Session
과 Request
기반 모두에서 사용될 수 있습니다.
RedirectHandler
RedirectHandler
프로토콜은 특정 Request
의 redirect 움직임을 컨트롤할 수 있도록 합니다. 한 가지 요구사항이 필요합니다.
func task(_ task: URLSessionTask,
willBeRedirectedTo request: URLRequest,
for response: HTTPURLResponse,
completion: @escaping (URLRequest?) -> Void)
이 메소드는 redirected URLRequest
를 수정하거나 redirect 전체를 허용하지 않도록 nil
을 통과시킬 수 있는 기회를 제공합니다. Alamofire는 RedirectHandler
를 준수하는 Redirector
타입을 제공합니다. 이 타입을 통해 쉬운 방법으로 redirected request를 따르게 하거나 따르지 않게 하거나 혹은 수정할 수 있게 합니다. Redirector
는 redirect 움직임을 컨트롤하는 Behavior
값을 갖고 있습니다.
public enum Behavior {
/// Follow the redirect as defined in the response.
case follow
/// Do not follow the redirect defined in the response.
case doNotFollow
/// Modify the redirect request defined in the response.
case modify((URLSessionTask, URLRequest, HTTPURLResponse) -> URLRequest?)
}
앞서 보였던 것처럼 Redirector
는 Session
과 Request
기반에 사용될 수 있습니다.
EventMonitor
sEventMonitor
프로토콜은 많은 양의 내부적인 Alamofire 이벤트들을 관찰하고 검사할 수 있도록 합니다. 많은 양의 내부적 Request
이벤트들은 물론 Alamofire로 구현되는 URLSessionDelegate
, URLSessionTaskDelegate
, URLSessionDownloadDelegate
를 포함합니다. default 상태에서, 작동하지 않는 빈 메소드인 위와 같은 이벤트들과 더불어, 성능을 유지하기 위해 전달되는 모든 이벤트들에 대해 EventMonitor
프로토콜은 DispatchQueue
를 요구합니다. 이 DispatchQueue
는 .main
이 default이지만, 타입을 준수하는 모든 custom은 전용 serial queue를 권장합니다.
EventMonitor
프로토콜의 가장 큰 용도는 관련 이벤트들의 로깅을 구현하는 것입니다. 아래와 같은 간단한 구현을 볼 수 있습니다.
final class Logger: EventMonitor {
let queue = DispatchQueue(label: ...)
// Event called when any type of Request is resumed.
func requestDidResume(_ request: Request) {
print("Resuming: \(request)")
}
// Event called whenever a DataRequest has parsed a response.
func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
debugPrint("Finished: \(response)")
}
}
이 Logger
타입은 위에서 보여주는 것과 같은 방법으로 Session
에 추가될 수 있습니다.
let logger = Logger()
let session = Session(eventMonitors: [logger])
프레임워크로써 Alamofire는 두 가지 주요 목적이 있습니다.
추상화를 통해 이 목적들을 달성하고 유용한 default를 제공하며 일반적인 작업들의 구현을 포함시킵니다. 그러나 Alamofire의 사용이 몇 가지 요청을 넘어서게 되면, 고수준을 넘어서는 것이 필요하게 되고 default 구현은 특정 앱에 대해 customized된 형태로 바뀌어야 합니다. Alamofire는 URLConvertible
과 URLRequestConvertible
프로토콜을 제공하고, 이 프로토콜들은 customization이 가능하도록 합니다.
URLConvertible
URLConvertible
프로토콜을 채택한 타입들은 내부적으로 URL Request들을 구성하기 위해 사용될 URL들을 구성하기 위해 사용될 수 있습니다. String
, URL
, URLComponents
는 default로 URLConvertible
을 준수하며, 이를 통해 이들이 request
, upload
, download
메소드에 url
파라미터 역할을 하는 형태로 전달될 수 있도록 합니다.
let urlString = "https://httpbin.org/get"
AF.request(urlString)
let url = URL(string: urlString)!
AF.request(url)
let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
AF.request(urlComponents)
중요한 방식으로 웹 앱과 상호작용하는 앱은 domain-specific model과 서버 리소스를 맵핑하기 위한 편리한 방식으로써 URLConvertible
에 준수하는 custom 타입들이 되도록 할 수 있습니다.
URLRequestConvertible
URLRequestConvertible
프로토콜을 채택하는 타입들은 URLRequest
를 구성하기 위해 사용될 수 있습니다.URLRequest
는 default
로 URLRequestConvertible
을 준수하며, request
, upload
, download
메소드들을 직접적으로 통과시킬 수 있도록 합니다. Alamofire는 URLRequestConvertible
을 request 파이프라인을 통한 흐름을 갖는 모든 request들의 기반 역할로 사용합니다. Alamofire가 제공하는 ParameterEncoder
의 외부에서 URLRequest
를 customize하기 위한 방법으로 URLRequest
를 직접 사용할 수 있습니다. 마지막 문장이 문서에 동사가 없어 번역이 매끄럽지 않을 수 있습니다.
let url = URL(string: "https://httpbin.org/post")!
var urlRequest = URLRequest(url: url)
urlRequest.method = .post
let parameters = ["foo": "bar"]
do {
urlRequest.httpBody = try JSONEncoder().encode(parameters)
} catch {
// Handle error.
}
urlRequest.headers.add(.contentType("application/json"))
AF.request(urlRequest)
중요한 방식으로 웹 앱과 상호작용하는 앱은 엔드포인트의 지속성을 보장할 수 있는 방식으로써 URLRequestConvertible
을 준수히나느 커스텀 타입들을 갖도록 할 수 있습니다. 이와 같은 방식은 server-side 불일치를 추상화하고, type-safe 라우팅을 제공하며, 다른 상태들을 관리합니다.
앱의 사이즈가 커지면, 네트워크 스택을 구축할 때 공통적인 패턴을 채택하는 것은 중요합니다. 이와 같은 디자인의 중요한 일부분은 어떻게 request들을 라우트할 것이냐에 관한 것입니다. Alamofire의 Router
디자인 패턴과 함께할 수 있는 URLConvertible
과 URLRequestConvertible
프로토콜이 도움이 될 것입니다.
"router"는 "routes"를 정의하는 타입이거나 request의 구성요소들입니다. 이 요소들은 URLRequest
의 일부, request를 만들기 위해 요구되는 파라미터들, 여러 request 각각에 대한 설정들을 포함할 수 있습니다. 간단한 라우터는 아래와 같습니다.
enum Router: URLRequestConvertible {
case get, post
var baseURL: URL {
return URL(string: "https://httpbin.org")!
}
var method: HTTPMethod {
switch self {
case .get: return .get
case .post: return .post
}
}
var path: String {
switch self {
case .get: return "get"
case .post: return "post"
}
}
func asURLRequest() throws -> URLRequest {
let url = baseURL.appendingPathComponent(path)
var request = URLRequest(url: url)
request.method = method
return request
}
}
AF.request(Router.get)
더 복잡한 라우터는 request의 파라미터들을 포함할 것입니다. ParameterEncoder
프로토콜과 인코더를 통해 모든 Encodable
타입이 파라미터로 사용될 수 있습니다.
enum Router: URLRequestConvertible {
case get([String: String]), post([String: String])
var baseURL: URL {
return URL(string: "https://httpbin.org")!
}
var method: HTTPMethod {
switch self {
case .get: return .get
case .post: return .post
}
}
var path: String {
switch self {
case .get: return "get"
case .post: return "post"
}
}
func asURLRequest() throws -> URLRequest {
let url = baseURL.appendingPathComponent(path)
var request = URLRequest(url: url)
request.method = method
switch self {
case let .get(parameters):
request = try URLEncodedFormParameterEncoder().encode(parameters, into: request)
case let .post(parameters):
request = try JSONParameterEncoder().encode(parameters, into: request)
}
return request
}
}
라우터들은 configure할 수 있는 속성들의 수에 관계없이 여러 엔드포인트에 대해 확장될 수 있습니다. 하지만 복잡성의 특정 수준이 과도하게 되면, 큰 라우터를 API의 부분들을 나타내는 작은 라우터들로 나누는 것이 좋을 수 있습니다.
Alamofire는 다양한 response
메소드와 ResponseSerializer
프로토콜을 통해 리스폰스 핸들링을 제공합니다.
ResponseSerializer
호출 없이도 DataRequest
와 DownloadRequest
는 리스폰스 핸들링이 가능한 메소드를 제공합니다. 이는 대용량 파일을 메모리에 로딩할 수 없는 DownloadRequest
에 대해 가장 중요한 부분입니다.
// DataRequest
func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse<Data?>) -> Void) -> Self
// DownloadRequest
func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDownloadResponse<URL?>) -> Void) -> Self
모든 리스폰스 핸들러처럼 모든 동기화 작업은 내부 queue와 메소드에 전달되는 queue
에 호출되는 컴플리션 핸들러에서 수행됩니다. 이는 default에서 main
queue에 다시 dispatch할 필요가 없음을 의미합니다. 그러나 컴플리션 핸들러에서 중요한 작업이 수행되는 경우, custom queue를 리스폰스 메소드에 전달하는 것을 권장하며, 필요한 경우 핸들러 자체에서 다시 main
으로 dispatch합니다.
ResponseSerializer
ResponseSerializer
프로토콜은 DataResponseSerializerProtocol
와 DownloadResponseSerializerProtocol
로 구성됩니다. 결합된 버전의 ResponseSerializer
는 아래와 같습니다.
public protocol ResponseSerializer: DataResponseSerializerProtocol & DownloadResponseSerializerProtocol {
/// The type of serialized object to be created.
associatedtype SerializedObject
/// `DataPreprocessor` used to prepare incoming `Data` for serialization.
var dataPreprocessor: DataPreprocessor { get }
/// `HTTPMethod`s for which empty response bodies are considered appropriate.
var emptyRequestMethods: Set<HTTPMethod> { get }
/// HTTP response codes for which empty response bodies are considered appropriate.
var emptyResponseCodes: Set<Int> { get }
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject
func serializeDownload(request: URLRequest?,
response: HTTPURLResponse?,
fileURL: URL?,
error: Error?) throws -> SerializedObject
}
default로 serializeDownload
메소드는 디스크로부터 다운로드된 Data
를 읽는 것과 serialize
를 호출하는 것을 통해 구현됩니다. 그러므로 앞서 언급한 DownloadRequest
의 response(queue:completionHandler:)
메소드를 사용하면서 대용량의 다운로드에 대한 custom 핸들링을 구현하는 것이 적합합니다.
ResponseSerializer
는 Alamofire에 내장된 다양한 ResponseSerializer
와 같은 타입을 따르면서도 customize될 수 있는 dataPreprocessor
, emptyResponseMethods
, emptyResponseCodes
를 위한 다양한 default 구현을 제공합니다.
모든 ResponseSerializer
사용은 DataRequest
와 DownloadRequest
에 대한 메소드를 통해 흐름이 이어집니다.
// DataRequest
func response<Serializer: DataResponseSerializerProtocol>(
queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void) -> Self
// DownloadRequest
func response<Serializer: DownloadResponseSerializerProtocol>(
queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDownloadResponse<Serializer.SerializedObject>) -> Void) -> Self
Alamofire는 아래와 같은 몇가지 리스폰스 핸들러를 포함합니다.
responseData(queue:completionHandler)
: DataResponseSerializer
를 사용함으로써 리스폰스 Data
를 검증하고 전처리합니다.responseString(queue:encoding:completionHandler:)
: 제공되어 있는 String.Encoding
을 사용하는 것을 통해 리스폰스 Data
를 String
으로 파싱합니다.responseJSON(queue:options:completionHandler)
: 제공되어 있는 JSONSerialization.ReadingOptions
를 사용함으로써 리스폰스 Data
를 파싱합니다. 이 메소드를 사용하는 것은 권장하지 않으며, 기존 Alamofire의 사용과 호환성을 위해서만 제공됩니다. 대신 responseDecodable
이 사용되어야 합니다.responseDecodable(of:queue:decoder:completionHandler:)
: 리스폰스 Data
를 제공된 혹은 추론되는 Decodable
타입으로 파싱하며, 이는 DataDecoder
의 사용하는 것을 통해 이뤄집니다. default로 JSONDecoder
를 사용하는 것이 좋습니다. JSON과 제너릭 리스폰스 파싱을 위한 권장하는 메소드입니다.DataResponseSerializer
Data
가 적합하게 반환(emptyResponseMethods
와 emptyResponseCodes
가 허용되지 않는 이상 비어있는 리스폰스가 아닌)되었다는 것을 검증하고 dataPreprocessor
를 통해 Data
를 전달하기 위해 DataRequest
혹은 DownloadRequest
에 대한 responseData(queue:completionHandler:)
는 DataResponseSerializer
를 사용합니다. 이 리스폰스 핸들러는 customize된 Data
핸들링에 유용하지만 필수적인 것은 아닙니다.
StringResponseSerializer
DataRequest
혹은 DownloadRequest
에 대해 responseString(queue:encoding:completionHandler)
를 호출하는 것은 Data
가 적합하게 반환(emptyResponseMethods
, emptyResponseCodes
에 의해 허가되지 않는 한 빈 리스폰스가 아니면)되었다는 것을 검증하고, Data
를 dataPreprocessor
를 통해 전달하기 위해 StringResponseSerializer
를 사용합니다. HTTPURLResponse
로부터 파싱된 String.Encoding
을 사용해 String
을 초기화하기 위해 전처리된 Data
가 사용됩니다.
JSONResponseSerializer
DataRequest
혹은 DownloadRequest
에 대해 responseJSON(queue:options:completionHandler)
를 호출하는 것은 Data
가 적합하게 반환(emptyResponseMethods
, emptyResponseCodes
에 의해 허가되지 않는 한 빈 리스폰스가 아니면)되었다는 것을 검증하고, Data
를 dataPreprocessor
를 통해 전달하기 위해 JSONResponseSerializer
를 사용합니다. 전처리된 Data
는 이후 제공된 선택사항에 따라 JSONSerialization.jsonObject(with:options:)
에 전달됩니다. 이 serializer는 권장하지 않습니다. 대신 DecodableResponseSerializer
를 사용하는 것아 더 나은 Swift 사용에 더 좋습니다.
DecodableResponseSerializer
DataRequest
혹은 DownloadRequest
에 대해 responseDecodable(of:queue:decoder:completionHandler)
를 호출하는 것은 Data
가 적합하게 반환(emptyResponseMethods
, emptyResponseCodes
에 의해 허가되지 않는 한 빈 리스폰스가 아니면)되었다는 것을 검증하고, Data
를 dataPreprocessor
를 통해 전달하기 위해 DecodableResponseSerializer
를 사용합니다. 전처리된 Data
는 제공된 DataDecoder
에 의해 전달되고, 제공된 혹은 추론된 Decodable
타입에 파싱됩니다.
Alamofire에 포함된 유용한 형태의 ResponseSerializer
와 더불어, 리스폰스 핸들링을 customize하기 위한 추가적인 방법이 존재합니다.
이미 있는 ResponseSerializer
를 사용하는 것과 이후 출력을 변형시키는 것은 리스폰스 핸들러를 customize하는 간단한 방법입니다. DataResponse
와 DownloadResponse
는 리스폰스와 연관된 메타데이터를 보존하면서도 리스폰스를 변형시킬 수 있는 메소드로 map
, tryMap
, mapError
, tryMapError
를 갖습니다. 예를 들어 어떠한 예전 파싱 에러를 보존하면서도, Decodable
리스폰스로부터 속성을 추출하는 것은 map
을 사용해 달성이 가능합니다.
AF.request(...).responseDecodable(of: SomeType.self) { response in
let propertyResponse = response.map { $0.someProperty }
debugPrint(propertyResponse)
}
error를 반환할 수 있는 변형은 tryMap
을 통해 사용될 수 있고, 아마도 검증을 수행하게 될 것입니다.
AF.request(..).responseDecodable(of: SomeType.self) { response in
let propertyResponse = response.tryMap { try $0.someProperty.validated() }
debugPrint(propertyResponse)
}
Alamofire가 제공하는 ResponseSerializer
나 리스폰스 변형이 충분히 유용하지 않을 때, 혹은 customization이 과도한 경우 ResponseSerializer
를 생성하는 것이 로직을 캡슐화하기 위한 방법으로 좋은 방법이 도리 것입니다. custom ResponseSerializer
를 통합하는 과정에 보통 두 가지가 존재하며, 순응하는 타입을 생성하는 것과 편리한 사용을 위한 Request
타입을 확장하는 것이 있습니다. 예를 들어 만약 서버가 특수한 형태로 인코딩된 String
을 반환한다면, 아마도 그것으 ㄴ값들이 콤마에 의해 분리되었을 것이고 이와 같은 형식의 ResponseSerializer
는 아래와 같을 수 있습니다.
struct CommaDelimitedSerializer: ResponseSerializer {
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> [String] {
// Call the existing StringResponseSerializer to get many behaviors automatically.
let string = try StringResponseSerializer().serialize(request: request,
response: response,
data: data,
error: error)
return Array(string.split(separator: ","))
}
}
SerializedObject
, associatedtype
두 가지 요구사항은 serialize
메소드에서 반환되는 타입에 의해 충족된다는 것을 기억하시기 바랍니다. 더 복잡한 serializer와 관련해, 반환 타입은 DecodableResponseSerializer
처럼 제너릭 타입의 동기화를 허용하는 제너릭이 될 수 있습니다.
CommaDelimitedSerializer
가 더 유용해지려면 추가적으로 필요한 것이 있습니다. 비어 있는 HTTP 메소드의 customization을 허용하는 것과 StringResponseSerializer
를 통해 메소드들을 전달하는 리스폰스 코드가 대표적입니다.
스티리밍 유형의 Data
를 받는 것을 처리하기 위해 DataStreamRequest
는 고유한 리스폰스 핸들러 타입을 사용합니다. 제공된 핸들러에 더불어, custom serialization이 DataStreamSerializer
프로토콜 사용을 통해 수행될 수 있습니다.
public protocol DataStreamSerializer {
/// Type produced from the serialized `Data`.
associatedtype SerializedObject
/// Serializes incoming `Data` into a `SerializedObject` value.
///
/// - Parameter data: `Data` to be serialized.
///
/// - Throws: Any error produced during serialization.
func serialize(_ data: Data) throws -> SerializedObject
}
스트리밍 Data
를 처리하기 위해 모든 custom DataStreamSerializer
가 사용될 수 있습니다. 이는 responseStream
메소드를 사용하는 것을 통해 이뤄집니다.
AF.streamRequest(...).responseStream(using: CustomSerializer()) { stream in
// Process stream.
}
Alamofire는 들어오는 Data
를 Decodable
타입으로 파싱할 수 있는 DecodableStreamSerializer
, DataStreamSerializer
를 포함합니다. 이는 DataDecoder
인스턴스와 DataPreprocessor
모두에서 customize될 수 있고, responseStreamDecodable
메소드를 통해 사용될 수 있습니다.
AF.streamRequest(...).responseDecodable(of: DecodableType.self) { stream in
// Process stream.
}
혹은 앞서 언급한 streamResponse
메소드를 직접 사용하는 것을 통해 이뤄집니다.
AF.streamRequest(...).responseStream(using: DecodableStreamSerializer<DecodableType>(decoder: JSONDecoder())) { stream in
// Process stream.
}
Combine 프레임워크를 지원하는 시스템에서 Alamofire는 custom Publisher
타입을 사용하는 것을 통해 리스폰스를 publish할 수 있습니다. 이 publisher는 Alamofire의 리스폰스 핸들러처럼 작동합니다. request와 연결되고 리스폰스 핸들러처럼 validate()
와 같은 API 뒤에 이어져야 합니다. 아래가 예시입니다.
AF.request(...).publishDecodable(type: DecodableType.self)
이 코드는 DataResponse<DecodableType, AFError>
을 publish하려는 DataResponsePublisher<DecodableType>
값을 제공합니다. Alamofire의 모든 Publisher
처럼 DataResponsePublisher
는 완전히 lazy합니다. 이는 다운스트림 Subsucriber
가 값을 요구할 때 리스폰스 핸들러를 추가하게 되는 것과 request를 resume
하는 것을 의미합니다. 오직 하나의 값만 제공하고 retry될 수 없습니다.
Alamofire의
Publisher
를 사용할 때 적합한 방법으로 retry를 다루려면, Alamofire의 retry 메커니즘을 사용해야 하고, 이는 앞서 설명한 above와 같은 것이 대표적입니다.
추가적으로 DataResponsePublisher
는 outgoing DataResponse<Success, Failure>
를 Result<Success, Failure>
값 혹은 Failure
를 동반하는 Success
값으로 변형시킵니다. 예를 들면 아래와 같습니다.
let publisher = AF.request(...).publishDecodable(type: DecodableType.self)
let resultPublisher = publisher.result() // Provides an AnyPublisher<Result<DecodableType, AFError>, Never>.
let valuePublisher = publisher.value() // Provides an AnyPublisher<DecodableType, AFError>.
모든 Publisher
처럼 DataResponsePublisher
는 다양한 Combine API와 함께 사용될 수 있습니다. 이를 통해 Alamofire는 한 번에 여러 request를 지원할 수 있게 됩니다.
// All usage of cancellable Combine API must have its token stored to maintain the subscription.
var tokens: Set<AnyCancellable> = []
...
let first = AF.request(...).publishDecodable(type: First.self)
let second = AF.request(...).publishDecodable(type: Second.self)
let both = Publishers.CombineLatest(first, second)
both.sink { first, second in // DataResponse<First, AFError>, DataResponse<Second, AFError>
debugPrint(first)
debugPrint(second)
}
.store(in: &tokens)
연달아 request를 수행하는 것도 가능합니다.
// All usage of cancellable Combine API must have its token stored to maintain the subscription.
var tokens: Set<AnyCancellable> = []
...
AF.request(...)
.publishDecodable(type: First.self)
.value()
.flatMap {
AF.request(...) // Use First value to create second request.
.publishDecodable(type: Second.self)
}
.sink { second in // DataResponse<Second, AFError>
debugPrint(second)
}
.store(in: &tokens)
subscribe가 발생하면, 변형의 연쇄는 첫 번째 request를 만들고 두 번째로 publisher를 생성합니다. 두 번째 request가 완료될 때 완전히 마무리됩니다.
Combine을 사용하는 모든 경우처럼,
sink
와 같은 함수에 의해 반환되는AnyCancellable
토큰의 생애주기를 유지하는 것을 통해 subscription이 이른 시점에 취소되지 않도록 해줘야합니다.
DownloadResponsePublisher
Alamofire는 DownloadRequest
, DownloadResponsePublisher
를 위한 Publisher
를 제공합니다. 이 Publisher
의 기능은 DataResponsePublisher
와 동일합니다.
대부분의 DownloadRequest
리스폰스 핸들러처럼,DownloadResponsePublisher
는 serialization을 수행하기 위해 디스크로부터 Data
를 읽습니다. 이는 대량의 Data
를 읽어야 할 때 시스템 성능에 영향일 미칠 수 있습니다. publishUnserialized()
를 사용해 파일이 다운로드된 URL?
만을 받고, 디스크로부터 대량의 파일을 읽는 것을 권장합니다.
DataStreamPublisher
DataStreamPublisher
는 DataStreamRequest
를 위한 Publisher
입니다. DataStreamRequest
처럼, 그리고 Alamofire의 다른 Publisher
와는 다르게, DataStreamPublisher
는 네트워크로는 물론 마지막 컴플리션 이벤트로부터 받은 Data
가 serialize된 여러 값을 반환할 수 있습니다. DataStreamRequest
가 어떻게 작동하는지에 대한 내용은 detailed usage documentation에서 볼 수 있습니다.
NetworkReachabilityManager
는 Cellular와 와이파이 네트워크 인터페이스 모두에서 호스트와 주소의 도달가능성에 대한 변화를 감지할 수 있습니다.
let manager = NetworkReachabilityManager(host: "www.apple.com")
manager?.startListening { status in
print("Network Status Changed: \(status)")
}
위 예시처럼
manager
를 유지해야함을 기억해야 합니다. 그렇지 않으면 상태의 변화가 보고되지 않습니다.
또한,host
string에 scheme을 포함하지 않아야 합니다. 그렇지 않으면 제대로 작동하지 않습니다.
네트워크 도달가능성을 사용할 때 어떤 것이 결정되어야 하는지에 대한 기억해야 할 것이 몇 가지 있습니다.
네트워크에 실패한 request들에 도달가능성으로 retry를 시도하는 것 대신, RetryPolicy
처럼 Alamofire에 내장된 RequestRetrier
를 사용하는 것이 더 간단하고 신뢰할 수 있는 방법일 것입니다. default로 RetryPolicy
는 오프라인 네트워크 연결을 포함한 다양한 error 조건들에 멱등성 request들을 retry할 것입니다.