Alamofire Advanced Usage

Panther·2021년 4월 27일
2

(검수중입니다.)

https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md

Advanced Usage

Alamofire는 URLSession과 Foundation의 URL Loading System을 기반으로 합니다. 이 프레임워크를 최대한 활용하려면 기본 네트워킹 스택의 개념과 기능에 익숙해지는 것을 권합니다.

읽으시기를 권장하는 글은 아래와 같습니다.

첫 번째 글은 이전에 번역한 적이 있어 아래 링크를 남기겠습니다.

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")

Creating Custom Session Instances

대부분의 앱이 다양한 방식으로 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할 수 있도록 합니다.

Creating a Session With a URLSessionConfiguration

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()을 호출할 것입니다. startRequestsImmediatelyfalse로 세팅하면 모든 Request는 resume()을 직접 호출해야 합니다..

let session = Session(startRequestsImmediately: false)

A Session’s DispatchQueues

default로 Session 인스턴스는 비동기 동작에 관해 하나의 DispatchQueue를 사용합니다. 이는 underlyingQueue를 포함하는데, 그것은URLSessiondelegate, 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여야 합니다. requestQueueserializationQueue는 serial 혹은 paralell일 수 있습니다. 성능 분석상 작업이 느려지지 않는 한 serial queue가 default이길 권장합니다. 이 경우 queue를 parallel하게 하면 전반적 성능에 도움이 될 수 있습니다.

Adding a RequestInterceptor

Alamofire의 RequestInterceptor 프로토콜(RequestAdapter & RequestRetrier)은 중요하고 강력한 Request adaptation과 retry 기능을 제공합니다. SessionRequest 레벨에서 적용될 수 있습니다. RequestInterceptor 자체와 Alamofire로 구현하는 다양한 방식은 RetryPolicy와 같은 아래 부분에서 살펴보시기 바랍니다.

below

let policy = RetryPolicy()
let session = Session(interceptor: policy) 

Adding a ServerTrustManager

Alamofire의 ServerTrustManager 클래스는 도메인과 ServerTrustEvaluating 타입을 따르는 인스턴스의 맵핑을 캡슐화합니다. 이를 통해 Session의 TLS 보안을 다루는 것을 customize할 수 있도록 합니다. 이는 인증서 사용, 키 고정, 인증서 해지 확인의 사용을 가능하게 합니다. 관련해 더 많은 것을 알고 싶으시다면 아래 ServerTrustManagerServerTrustEvaluating 부분을 살펴보시기 바랍니다. ServerTrustManager를 초기화하는 것은 도메인과 수행하게 될 평가 타입 사이 맵핑을 나타냄으로써 쉽게 할 수 있습니다.

let manager = ServerTrustManager(evaluators: ["httpbin.org": PinnedCertificatesTrustEvaluator()])
let session = Session(serverTrustManager: manager)

더 자세한 부분은 아래 내용에서 살펴보시기 바랍니다.

below

Adding a RedirectHandler

Alamofire의 RedirectHandler 프로토콜은 HTTP redirect 리스폰스를 다루는 것을 customize할 수 있도록 도와줍니다. SessionRequest 레벨 모두에서 적용될 수 있습니다. Alamofire는 RedirectHandler를 준수하는 Redirector 타입을 갖고 있으며, redirect에 있어 간단한 컨트롤을 제공합니다. RedirectHandler에 대해 더 알고 싶으시다면 아래 내용을 참고하시기 바랍니다.

below

let redirector = Redirector(behavior: .follow)
let session = Session(redirectHandler: redirector)

Adding a CachedResponseHandler

Alamofire의 CachedResponseHandler 프로토콜은 리스폰스의 caching을 customize하며, 이는 SessionRequest 레벨 모두에서 적용될 수 있습니다. Alamofire는 ResponseCacher 타입을 갖고 있고 이 타입은 CachedResponseHandler를 준수합니다. 이를 통해 리스폰스 caching의 간단한 컨트롤을 제공합니다. 아래 내용에서 자세한 내용을 보실 수 있습니다.

below

let cacher = ResponseCacher(behavior: .cache)
let session = Session(cachedResponseHandler: cacher)

Adding EventMonitors

Alamofire는 내부의 이벤트에 관한 강력한 인사이트를 주는 EventMonitor 프로토콜을 갖고 있습니다. EventMonitor는 로깅과 기타 이벤트 기반의 기능을 제공하는 데 사용됩니다. Session은 초기화될 때 EventMonitor 인스턴스를 따르는 배열을 허용합니다.

let monitor = ClosureEventMonitor()
monitor.requestDidCompleteTaskWithError = { (request, task, error) in
    debugPrint(request)
}
let session = Session(eventMonitors: [monitor])

Operating on All Requests

사용은 드물겠지만 Session은 active 상태의 모든 Request에 대해 제어할 수 있는 withAllRequests 메소드를 제공합니다. SessionrootQueue에서 작동하게 될 것이기 때문에 빠르게 유지하는 것이 중요합니다. 만약 동작에 시간이 걸린다면 RequestSet을 처리하기 위해 분리된 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에 대해 작업이 수행될 것이라고 생각하면 안 됩니다.

Creating Instances From URLSessions

앞서 설명한 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를 가져야만 하고 이 DispatchQueueSessionrootQueue로써 전달되어야 합니다.

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)

Requests

Alamofire에서 기능하는 각각의 Request는 특정 클래스에 의해 캡슐화되며,DataRequest, UploadRequest, DownloadRequest입니다. 이와 같은 클래스는 각각의 Request 타입에 맞춰 고유한 기능을 캡슐화합니다. 그런데 DataRequest, DownloadRequest는 공통 상위 클래스로부터 상속됩니다.(UploadRequestDataRequest로부터 상속) Request 인스턴스는 직접적으로 생성되지 않고, 대신 Session 인스턴스로부터 다양한 Request 메소드 중 하나를 통해 내보내집니다.

The Request Pipeline

초기 파라미터 혹은 URLRequestConvertible와 함께 Request 서브클래스가 한 번 생성되면, Alamofire의 Request 파이프라인을 구성하는 일련의 단계를 거치게 됩니다. Request가 성공적이면 아래 내용을 포함합니다.

  1. HTTP 메소드, 헤더, 기타 파라미터와 같은 초기 파라미터는 내부의 URLRequestConvertible 값에 캡슐화됩니다. 만약 URLRequestConvertible값이 직접적으로 전달된다면, 그 값은 변경되지 않고 사용됩니다.

  2. asURLRequest()URLRequestConvertible 값에서 호출되며, 첫 번째 URLRequest값을 생성합니다. 이 값은 Request에 전달되고 requests에 저장됩니다. 만약 URLRequestConvertible값이 Session 메소드에서 전달된 파라미터로부터 생성되었다면, URLRequest가 생성될 때 모든 RequestModifier가 호출됩니다.

  3. 만약 Session 혹은 Request, RequestAdpter 혹은 RequestInterceptor가 하나라도 있다면, 이전에 생성된 URLRequest를 사용하면서 호출될 것입니다. 적용된 URLRequestRequest에 전달되고 request에 저장됩니다.

  4. URLRequest에 기반한 네트워크 request를 수행하려면 URLSessionTask를 생성해야 합니다. 이를 위해 SessionRequest를 호출합니다.

  5. URLSessionTask가 완료되고 URLSessionTaskMetrics가 모이면, RequestValidator를 수행합니다.

  6. Request는 모든 리스폰스 핸들러를 수행합니다. 추가된 responseDecodable과 같은 것이 있습니다.

단계 중에서 실패가 나타날 수 있으며, Error 값을 생성 혹은 받는 것을 통해 알 수 있고 이 Error는 연관이 있는 Request에 전달됩니다. 예를 들어 1단계에서 4단계까지를 제외하고 모든 단계는 Error를 생성할 수 있으며, 리스폰스 핸들러에 전달되거나 retry를 해볼 수 있을 것입니다. 아래에서 Request 파이프라인을 통해서 실패할 수 있거나 혹은 실패하지 않는 몇 가지 예를 살펴볼 수 있습니다.

  1. 파라미터 캡슐화가 실패할 수 있습니다.

  2. asURLRequest()가 호출될 때 URLRequestConvertible값이 에러를 생성할 수 있습니다. 이는 다양한 URLRequest 프로퍼티에 대한 초기 검증이나 파라미터 인코딩 실패를 가능하게 합니다.

  3. RequestAdapter는 적용되는 동안 실패할 수 있고, 아마도 인증 토큰을 잃어버렸을 때 실패가 발생할 것입니다.

  4. URLSessionTask 생성이 실패할 수 있습니다.

  5. URLSessionTask는 다양한 이유로 인해서 오류와 함께 완료될 수 있으며, 이는 네트워크 가용과 취소를 포함합니다. 이 Error 값들은 Request로 되돌아갑니다.

  6. 리스폰스 핸들러는 Error를 생성할 수 있으며, 보통 타당하지 않은 리스폰스나 다른 파싱 에러에 기인합니다.

에러가 Request에 전달될 때, RequestSession 혹은 Request와 연관이 있는 RequestRetrier를 시도할 수 있습니다. 만약 RequestRetrierRequest를 retry하기를 선택한다면, 완료된 파이프라인이 다시 작동합니다. RequestRetrierError를 생성할 수도 있으며 retry를 시도하지 않습니다.

Request

Request가 특정 request의 유형을 캡슐화하지 않을지라도, Alamofire가 수행하는 모든 request에 대한 공통적인 상태와 기능을 갖고 있을 것입니다. 아래와 같은 내용을 포함합니다.

State

모든 Request 유형은 상태 개념을 포함하고 있으며, Request의 lifetime 안에서 주요 이벤트를 나타나도록 할 것입니다.

public enum State {
    case initialized
    case resumed
    case suspended
    case cancelled
    case finished
}

Request는 생성 후 .initialized 상태에서 시작합니다. Request는 멈춰지거나 재개할 수 있고 취소될 수도 있으며, 걸맞는 lifetime 메소드를 호출하는 것을 통해 이뤄집니다.

  • resume() 메소드는 Request의 네트워크 트래픽을 재개하거나 시작합니다. 만약 startRequestsImmediatelytrue이면, 리스폰스 핸들러가 Request에 추가되었을 때 자동으로 호출됩니다.

  • suspend() 메소드는 Request의 네트워크 트래픽을 suspend하거나 일시정지합니다. 이 상태의 Request는 재개될 수 있으나 오직 DownloadRequests만 데이터 전달을 지속할 수 있을 것입니다. 다른 Request는 다시 시작됩니다.

  • cancel() 메소드는 Request를 취소합니다. 이 상태에 있으면 Request는 재개되거나 중단될 수 없습니다. cancel()메소드가 호출될 때 Requesterror 프로퍼티가 AFError.explicitlyCancelled 인스턴스와 함께 설정될 것입니다. 만약 Request가 재개되고 이후 취소되지 않는다면, 모든 리스폰스 validator와 리스폰스 serializer가 실행됐을 때 .finished 상태에 도달할 것입니다. 그러나 .finished 상태에 Request가 도달했을 때 추가적인 리스폰스 serializer가 Request에 추가되면, .resumed상태로 전환되고 네트워크 request를 다시 수행할 것입니다.

Progress

request의 progress를 추적하기 위해 RequestuploadProgressdownloadProgress 속성을 제공하고, 클로저 기반의 uploadProgressdownloadProgress 메소드를 제공합니다. 모든 클로저 기반의 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를 갖도록 해야 할 것입니다.

  • upload progress에서 progress는 아래와 같은 방식으로 결정될 수 있습니다.
    • upload body로 UploadRequest에 제공되는 Data 객체의 길이에 의해
    • UploadRequest의 upload body로써 제공된 디스크의 파일 길에 의해
    • request에 대한 Content-Length 헤더의 값에 의해(만약 직접적으로 설정했다면)
  • download progress는 하나의 요구사항이 있습니다.
    • 서버 리스폰스가 Content-Length 헤더를 포함해야만 함

안타깝게도 URLSession으로부터 progress 보고를 받으려고 할 때, 정확한 보고를 방해할 수 있는 요구사항들이 있으며 이 요구사항들은 문서화되어있지 않습니다.

Handling Redirects

Alamofire의 RedirectHandler 프로토콜은 Request에 대한 redirect handling의 컨트롤과 customization을 제공합니다. SessionRedirectHandler에 추가하여 각각의 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 결과가 나타날 것입니다.

Customizing Caching

Alamofire의 CachedResponseHandler 프로토콜은 리스폰스 캐싱에 대한 컨트롤과 customization을 제공합니다. SessionCachedResponseHandler에 추가하여 각각의 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 결과가 나타날 것입니다.

Credentials

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)
    }

Lifetime Values

Alamofire는 Request의 lifetime 동안 다양한 underlying 값을 생성합니다. 대부분 내부에 있는 구현에 대한 세부 정보들이며, URLRequestURLSessionTask는 다른 API와 직접적으로 상호작용할 수 있는 상태가 될 것입니다.

A Request’s URLRequests

Request로 발행되는 각각의 네트워크 request는 URLRequest 값에서 캡슐화되며, 이 값은 하나의 Session request 메소드에 전달된 다양한 파라미터로부터 생성됩니다. Request는 스스로 갖고 있는 requests 배열 속성에 URLRequest의 사본을 보관하게 될 것입니다. 이와 같은 값들은 보냈던 파라미터로부터 생성된 초기 URLRequest를 포함하게 될 것이며, RequestInterceptor에 의해 생성된 URLRequest 역시 포함하게 될 것입니다. 그러나 이 배열은 Request를 대신해서 발행된 URLSessionTask가 수행하는 URLRequest는 포함하지 않을 것입니다. 이 값들을 파악하려면 tasks 프로퍼티가 Request에 의해 수행되는 모든 URLSessionTasks에 접근 권한을 줍니다.

이 값들을 축적하는 것과 더불어, 모든 RequestRequest에서 URLRequest가 생성될 때마다 클로저를 호출하는 onURLRequestCreation 메소드를 갖습니다. 이 URLRequestSessionrequest메소드에 전달된 초기 파라미터의 결과이자 RequestInterceptor에 의해 적용되는 변경내역이기도 합니다. 만약 Request가 retry되면 여러번에 걸쳐 호출될 것이며, 한 번에 하나의 클로저가 설정될 수 있습니다. URLRequest 값은 이 클로저에서 수정될 수 없으며, URLRequest가 발행되기 전에 수정이 필요하다면 RequestInterceptor를 사용하거나 Alamofire에 전달되기 전에 URLRequestConvertible 프로토콜을 사용하는 request를 구성해야 합니다.

AF.request(...)
    .onURLRequestCreation { request in
        print(request)
    }
    .responseDecodable(of: SomeType.self) { response in
        debugPrint(response)
    }
URLSessionTasks

많은 방식으로 다양한 Request 서브클래스가 URLSessionTask의 wrapper로써 기능하며, 다양한 유형의 task와 상호작용하기 위한 구체적인 API를 제공합니다. 이와 같은 task들은 tasks 배열 속성을 통해 Request 인스턴스에서 보여지도록 만들 수 있습니다. 이는 Request에서 생성된 초기 task를 포함하며, retry 하나당 retry 프로세스의 부분으로써 생성된 모든 후속 task를 포함합니다.

이 값들을 축적시키는 것과 더불어 모든 RequestRequest에서 생성되는 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)
    }

Response

각각의 Request는 아마 request가 완료되었을 때 이용 가능한 HTTPURLResponse 값을 갖게 될 것입니다. 이 값은 request가 취소되지 않았었고 네트워크 request에 실패하지 않았을 때에만 이용이 가능합니다. 추가적으로 만약 request가 retry 되면, 마지막 리스폰스만 이용이 가능합니다. 중간 리스폰스는 tasks 프로퍼티에서 URLSessionTask로부터 파생될 수 있습니다.

URLSessionTaskMetrics

Alamofire는 Request에서 수행되는 모든 URLSessionTaskURLSessionTaskMetrics 값을 수집합니다. 이 값들은 metrics 프로퍼티에서 이용 가능하며, 각각의 값은 같은 인덱스에 있는 tasksURLSessionTask에 해당합니다.

URLSessionTaskMetricsDataResponse와 같은 Alamofire의 다양한 리스폰스 타입들에서 이용 가능하도록 만들어져 있습니다.

AF.request(...)
    .responseDecodable(of: SomeType.self) { response in {
        print(response.metrics)
    }

FB7624529 때문에 7 아래 버전의 watchOS에서 URLSessionTaskMetrics의 수집은 불가능한 상태입니다.

DataRequest

DataRequest는 메모리에 저장된 Data에 서버 리스폰스를 다운로드하는 URLSessionDataTask를 캡슐화하는 Request의 서브클래스입니다. 그러므로 극도로 거대한 다운로드는 시스템 성능에 부정적인 영향을 줄 수 있다는 것을 알아야 합니다. 그러한 다운로드는 DownloadRequest를 사용함으로써 디스크에 데이터를 저장하길 권장합니다.

Additional State

DataRequestRequest에서 제공하는 프로퍼티 외에 몇 가지 프로퍼티를 가지고 있습니다. 서버 리스폰스로부터 Data를 축적된 형태의 data를 포함합니다. 그리고 인스턴스 생성 시 오리지널 파라미터를 포함하고 있는 상태인 DataRequest가 생성된 URLRequestConvertible도 포함하고 있습니다.

Validation

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

DataStreamRequestRequest의 서브클래스이며, URLSessionDataTask를 캡슐화하고 HTTP 연결로부터 시간의 흐름에 따라 Data를 스트리밍합니다.

Additional State

DataStreamRequest는 추가적인 public 상태를 포함하지 않습니다.

Validation

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

UploadRequestDataRequest의 서브클래스이며, URLSessionUploadTask를 캡슐화하고 Data 값, 디스크의 파일을 업로드하거나 원격 서버에 InputStream을 업로드합니다.

Additional State

UploadRequestDataRequest에서 제공하는 프로퍼티 외에 몇 가지 프로퍼티를 갖고 있습니다. 파일 업로드 시 디스크 접근과 관련해 customize하기 위해 사용되는 FileManager 인스턴스를 포함하고 있고, request를 설명하는 데 사용되는URLRequestConvertible값과 업로드가 수행될 때 유형을 결정할 수 있는 Uploadable 값도 갖고 있습니다.

DownloadRequest

DownloadRequestRequest의 구체적인 서브클래스이며, 디스크에 리스폰스 Data를 다운로드를 진행하는 URLSessionDownloadTask를 캡슐화합니다.

Additional State

DownloadRequestRequest에서 제공하는 프로퍼티 외에 몇 가지 프로퍼티를 갖고 있습니다. Data가 제공되던 중 취소된 이후 다운로드를 재개하는 데 사용되는 파라미터인 resumeData를 포함하고 있고, 다운로드 완료가 되었을 때 어디에서 다운로드된 파일을 이용할 수 있는지 구체화해주는 URLfileURL도 포함하고 있습니다.

Cancellation

Request가 제공하는 cancel() 메소드와 더불어 DownloadRequestcancel(producingResumeData shouldProduceResumeData: Bool)를 제공하며, 이 메소드는 가능한 경우 선택적으로 resumeData 프로퍼티를 생성할 수 있습니다. 그리고 cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void) 메소드는 전달된 클로저에 생성된 resume 데이터를 재공합니다.

AF.download(...)
    .cancel { resumeData in
        ...
    }

Validation

DownloadRequestDataRequestUploadRequest에 비해 조금 다른 버전의 검증을 지원합니다. 이는 데이터가 디스크에 다운로드된다는 사실때문입니다.

public typealias Validation = (_ request: URLRequest?, _ response: HTTPURLResponse, _ fileURL: URL?)

다운로드된 Data에 직접 접근하는 것 대신 주어진 fileURL을 사용해 접근되어야 합니다. 그렇게 하지 않으면 DownloadRequest 검증 기능은 DataRequest의 검증 기능과 동일하게 될 것입니다.

Adapting and Retrying Requests with RequestInterceptor

Alamofire의 RequestInterceptor 프로토콜(RequestAdapterRequestRetrier 프로토콜로 구성된)은 강력한 세션 기능과 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: 초기에 생성된 URLRequestRequest를 생성하기 위해 사용되었던 파라미터 혹은 URLRequestConvertible 값으로부터 생성된 것입니다.

  • session: Session adapter가 호출되는 요청을 생선한 Session입니다.

  • completion: adapter가 마무리되었다는 것을 나타내기 위해 반드시 호출되어야 하는 비동기 컴플리션 핸들러입니다. 비동기라는 특성을 통해 Request가 네트워크에 전달되기 전에, RequestAdapter는 네트워크나 디스크로부터 비동기로 리소스에 접근할 수 있습니다. completion 클로저로부터 제공된ResultURLRequest 값이 수정된 형태로 .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 프로토콜은 RequestError를 맞이했을 때 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)
    }
}

Using Multiple RequestInterceptors

Alamofire는 여러개의 RequestInterceptor를 사용할 수 있도록 지원합니다. SessionRequest 레벨 모두에서 그렇게 할 수 있으며, Interceptor 타입을 사용하는 것을 통해 구현할 수 있습니다. Interceptor는 adapter와 retrier 클로저를 통해 구성될 수 있고, RequestAdapterRequestRetrier의 조합, 혹은 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)

Security

서버 및 웹 서비스와 소통할 때 보안 HTTPS 연결을 사용하는 것은 민감한 데이터를 보호하는 중요한 단계입니다. default로 Alamofire는 동일한 자동 TLS 인증서와 URLSession으로써 인증서 체인 검증을 받습니다. 이는 인증서 체인이 유효함을 보장하면서도, man-in-the-middle(MITM) 공격 혹은 다른 잠재적 취약성을 보호하지는 않습니다. MITM 공격을 완화하려면 고객의 민감한 데이터를 다루거나 금융 정보 같은 데이터를 다루는 앱은 인증서나 공용 키 고정을 사용해야만 하며, Alamofire의 ServerTrustEvaluating 프로토콜을 통해 구현해야 합니다.

Evaluating Server Trusts with 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 값의 배열을 평가하고, 배열의 모든 것이 성공적일 때에만 성공한 것으로 평가합니다. 이 타입은 예를 들어 RevocationTrustEvaluatorPinnedCertificatesTrustEvaluator와 같은 것을 조합하기 위해 사용될 수 있습니다.

  • DisabledTrustEvaluator: 이 타입은 다른 타입 모두의 평가를 비활성화하기 때문에 디버그 시나리오에서만 사용되어야 합니다. 이는 모든 서버 신뢰를 항상 유효한 것으로 간주합니다. 이 타입은 절대로 production environments에서 사용하면 안 됩니다.

ServerTrustManager

ServerTrustManagerServerTrustEvaluating 값과 특정 호스트의 내부적 맵핑을 저장하는 역할을 합니다. 이를 통해 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가 성공적임을 수락하기 위해서 아래 기준들이 충족시길 요구합니다.
    • 인증서 체인이 반드시 유효해야 합니다.
    • 인증서 체인이 고정된 인증서들 중 한 가지를 포함해야 합니다.
    • 인증하려는 호스트가 인증 체인의 leaf 인증서에 있는 호스트와 동일해야 합니다.
  • keys.example.com은 항상 default 및 인증서 고정이 활성화되어 있는 상태에서 인증서 고정을 사용할 것입니다. 그러므로 TLS handshake가 성공적임을 수락하기 위해서 아래 기준들이 충족시길 요구합니다.
    • 인증서 체인이 반드시 유효해야 합니다.
    • 인증서 체인이 고정된 공용 키들 중 한 가지를 포함해야 합니다.
    • 인증하려는 호스트는 인증서 체인의 leaf 인증서에 있는 호스트와 동일해야 합니다.
  • 다른 호스트에 대한 Request들은 error를 생성할 것이고, 이는 ServerTrustManager가 default로 모든 호스트에 대한 평가를 요구하기 때문입니다.
Subclassing Server Trust Policy Manager

일치 동작에 관해 더 유연한 서버 신뢰 정책이 필요하다면, 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
    }
}

App Transport Security

iOS 9에서 App Transport Security (ATS)의 추가와 함께, 여러 ServerTrustEvaluating 객체를 사용하는 custom ServerTrustManager는 효과가 없을 것입니다. 만약 CFNetwork SSLHandshake failed (-9806) error들을 지속적으로 보게 된다면, 이 문제에 부딪힌 경우입니다. 앱의 plist에서 앱이 서버 신뢰를 평가하게끔 충분히 비활성화하도록 앱의 ATS 설정을 configure하지 않는 한, 애플의 ATS 시스템은 전체 challenge 시스템을 override합니다. 이 문제(높은 확률로 self-signed 인증서일 것)를 만나면, NSAppTransportSecurity overridesInfo.plist에 추가하는 것을 통해 문제를 해결할 수 있을 것입니다. nscurl 툴의 --ats-diagnotics 옵션을 사용해 어떤 ATS override가 필요한지 보기 위한 일련의 호스트에 대한 테스트를 수행할 수 있습니다.

Using Self-Signed Certificates with Local Networking

로컬호스트에서 작동하는 서버에 연결을 시도한다면, 그리고 self-signed 인증서들을 사용하려고 한다면, Info.plist에 아래 내용을 추가해야 합니다.

<dict>
    <key>NSAppTransportSecurity</key>
    <dict>
        <key>NSAllowsLocalNetworking</key>
        <true/>
    </dict>
</dict>

Apple documentation에 따르면, 앱에서 ATS를 비활성화하는 것 없이 NSAllowsLocalNetworkingYES로 설정하는 것을 통해 로컬 리소스를 로딩할 수 있습니다.

Customizing Caching and Redirect Handling

URLSessionURLSessionDataDelegateURLSessionTaskDelegate을 사용함을 통해 캐싱 움직임과 redirect 움직임을 customization할 수 있도록 합니다. Alamofire는 CachedResponseHandlerRedirectHandler 프로토콜로 customization 포인트들을 표시합니다.

CachedResponseHandler

CachedResponseHandler 프로토콜은 request를 생성 Session과 관련이 있는 URLCache 인스턴스에서 HTTP 리스폰스를 캐싱하는 것에 대한 컨트롤이 가능하도록 합니다. 하나의 요구사항이 필요합니다.

func dataTask(_ task: URLSessionDataTask,
              willCacheResponse response: CachedURLResponse,
              completion: @escaping (CachedURLResponse?) -> Void)

메소드 시그니처에서 보여지는 것처럼, 이 컨트롤은 네트워크 transfer를 위해 URLSessionDataTask를 사용하는 Request에만 적용됩니다. 여기서 네트워크 transfer는 DataRequest,UploadRequest (URLSessionUploadTaskURLSessionDataTask 서브클래스이기 때문)를 포함합니다. 캐싱을 위해 리스폰스를 고려할 때 생각할 수 있는 조건은 여러 가지가 있습니다. 그렇기 때문에 URLSessionDataDelegate 메소드 urlSession(_:dataTask:willCacheResponse:completionHandler:)를 다시 살펴보는 것이 중요합니다. 캐싱을 위한 리스폰스를 고려할 때, 만들 수 있는 다양한 방법들이 있습니다.

  • nil CachedURLResponse를 반환하는 것을 통해 리스폰스 캐싱을 방지할 수 있습니다.
  • 캐시값이 어디에 있어야 할지 변경시키기 위한 CachedURLResponsestoragePolicy를 수정할 수 있습니다.
  • 값들을 추가하거나 제거함으로써 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?)
}

위 내용처럼 ResponseChacerSessionRequest 기반 모두에서 사용될 수 있습니다.

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?)
}

앞서 보였던 것처럼 RedirectorSessionRequest 기반에 사용될 수 있습니다.

Using EventMonitors

EventMonitor 프로토콜은 많은 양의 내부적인 Alamofire 이벤트들을 관찰하고 검사할 수 있도록 합니다. 많은 양의 내부적 Request 이벤트들은 물론 Alamofire로 구현되는 URLSessionDelegate, URLSessionTaskDelegate, URLSessionDownloadDelegate를 포함합니다. default 상태에서, 작동하지 않는 빈 메소드인 위와 같은 이벤트들과 더불어, 성능을 유지하기 위해 전달되는 모든 이벤트들에 대해 EventMonitor 프로토콜은 DispatchQueue를 요구합니다. 이 DispatchQueue.main이 default이지만, 타입을 준수하는 모든 custom은 전용 serial queue를 권장합니다.

Logging

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])

Making Requests

프레임워크로써 Alamofire는 두 가지 주요 목적이 있습니다.

  1. 프로토타입과 툴에 대한 네트워크 Request 구현을 쉽게 하는 것입니다.
  2. 앱 네트워킹의 토대 역할을 합니다.

추상화를 통해 이 목적들을 달성하고 유용한 default를 제공하며 일반적인 작업들의 구현을 포함시킵니다. 그러나 Alamofire의 사용이 몇 가지 요청을 넘어서게 되면, 고수준을 넘어서는 것이 필요하게 되고 default 구현은 특정 앱에 대해 customized된 형태로 바뀌어야 합니다. Alamofire는 URLConvertibleURLRequestConvertible 프로토콜을 제공하고, 이 프로토콜들은 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를 구성하기 위해 사용될 수 있습니다.URLRequestdefaultURLRequestConvertible을 준수하며, 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 라우팅을 제공하며, 다른 상태들을 관리합니다.

Routing Requests

앱의 사이즈가 커지면, 네트워크 스택을 구축할 때 공통적인 패턴을 채택하는 것은 중요합니다. 이와 같은 디자인의 중요한 일부분은 어떻게 request들을 라우트할 것이냐에 관한 것입니다. Alamofire의 Router 디자인 패턴과 함께할 수 있는 URLConvertibleURLRequestConvertible 프로토콜이 도움이 될 것입니다.

"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의 부분들을 나타내는 작은 라우터들로 나누는 것이 좋을 수 있습니다.

Response Handling

Alamofire는 다양한 response 메소드와 ResponseSerializer 프로토콜을 통해 리스폰스 핸들링을 제공합니다.

Handling Responses Without Serialization

ResponseSerializer 호출 없이도 DataRequestDownloadRequest는 리스폰스 핸들링이 가능한 메소드를 제공합니다. 이는 대용량 파일을 메모리에 로딩할 수 없는 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 프로토콜은 DataResponseSerializerProtocolDownloadResponseSerializerProtocol로 구성됩니다. 결합된 버전의 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를 호출하는 것을 통해 구현됩니다. 그러므로 앞서 언급한 DownloadRequestresponse(queue:completionHandler:) 메소드를 사용하면서 대용량의 다운로드에 대한 custom 핸들링을 구현하는 것이 적합합니다.

ResponseSerializer는 Alamofire에 내장된 다양한 ResponseSerializer와 같은 타입을 따르면서도 customize될 수 있는 dataPreprocessor, emptyResponseMethods, emptyResponseCodes를 위한 다양한 default 구현을 제공합니다.

모든 ResponseSerializer 사용은 DataRequestDownloadRequest에 대한 메소드를 통해 흐름이 이어집니다.

// 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을 사용하는 것을 통해 리스폰스 DataString으로 파싱합니다.
  • responseJSON(queue:options:completionHandler): 제공되어 있는 JSONSerialization.ReadingOptions를 사용함으로써 리스폰스 Data를 파싱합니다. 이 메소드를 사용하는 것은 권장하지 않으며, 기존 Alamofire의 사용과 호환성을 위해서만 제공됩니다. 대신 responseDecodable이 사용되어야 합니다.
  • responseDecodable(of:queue:decoder:completionHandler:): 리스폰스 Data를 제공된 혹은 추론되는 Decodable 타입으로 파싱하며, 이는 DataDecoder의 사용하는 것을 통해 이뤄집니다. default로 JSONDecoder를 사용하는 것이 좋습니다. JSON과 제너릭 리스폰스 파싱을 위한 권장하는 메소드입니다.

DataResponseSerializer

Data가 적합하게 반환(emptyResponseMethodsemptyResponseCodes가 허용되지 않는 이상 비어있는 리스폰스가 아닌)되었다는 것을 검증하고 dataPreprocessor를 통해 Data를 전달하기 위해 DataRequest 혹은 DownloadRequest에 대한 responseData(queue:completionHandler:)DataResponseSerializer를 사용합니다. 이 리스폰스 핸들러는 customize된 Data 핸들링에 유용하지만 필수적인 것은 아닙니다.

StringResponseSerializer

DataRequest 혹은 DownloadRequest에 대해 responseString(queue:encoding:completionHandler)를 호출하는 것은 Data가 적합하게 반환(emptyResponseMethods, emptyResponseCodes에 의해 허가되지 않는 한 빈 리스폰스가 아니면)되었다는 것을 검증하고, DatadataPreprocessor를 통해 전달하기 위해 StringResponseSerializer를 사용합니다. HTTPURLResponse로부터 파싱된 String.Encoding을 사용해 String을 초기화하기 위해 전처리된 Data가 사용됩니다.

JSONResponseSerializer

DataRequest 혹은 DownloadRequest에 대해 responseJSON(queue:options:completionHandler)를 호출하는 것은 Data가 적합하게 반환(emptyResponseMethods, emptyResponseCodes에 의해 허가되지 않는 한 빈 리스폰스가 아니면)되었다는 것을 검증하고, DatadataPreprocessor를 통해 전달하기 위해 JSONResponseSerializer를 사용합니다. 전처리된 Data는 이후 제공된 선택사항에 따라 JSONSerialization.jsonObject(with:options:)에 전달됩니다. 이 serializer는 권장하지 않습니다. 대신 DecodableResponseSerializer를 사용하는 것아 더 나은 Swift 사용에 더 좋습니다.

DecodableResponseSerializer

DataRequest 혹은 DownloadRequest에 대해 responseDecodable(of:queue:decoder:completionHandler)를 호출하는 것은 Data가 적합하게 반환(emptyResponseMethods, emptyResponseCodes에 의해 허가되지 않는 한 빈 리스폰스가 아니면)되었다는 것을 검증하고, DatadataPreprocessor를 통해 전달하기 위해 DecodableResponseSerializer를 사용합니다. 전처리된 Data는 제공된 DataDecoder에 의해 전달되고, 제공된 혹은 추론된 Decodable 타입에 파싱됩니다.

Customizing Response Handlers

Alamofire에 포함된 유용한 형태의 ResponseSerializer와 더불어, 리스폰스 핸들링을 customize하기 위한 추가적인 방법이 존재합니다.

Response Transforms

이미 있는 ResponseSerializer를 사용하는 것과 이후 출력을 변형시키는 것은 리스폰스 핸들러를 customize하는 간단한 방법입니다. DataResponseDownloadResponse는 리스폰스와 연관된 메타데이터를 보존하면서도 리스폰스를 변형시킬 수 있는 메소드로 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)
}

Creating a Custom Response Serializer

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를 통해 메소드들을 전달하는 리스폰스 코드가 대표적입니다.

Streaming Response Handlers

스티리밍 유형의 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는 들어오는 DataDecodable 타입으로 파싱할 수 있는 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.
}

Using Alamofire with Combine

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

DataStreamPublisherDataStreamRequest를 위한 Publisher입니다. DataStreamRequest처럼, 그리고 Alamofire의 다른 Publisher와는 다르게, DataStreamPublisher는 네트워크로는 물론 마지막 컴플리션 이벤트로부터 받은 Data가 serialize된 여러 값을 반환할 수 있습니다. DataStreamRequest가 어떻게 작동하는지에 대한 내용은 detailed usage documentation에서 볼 수 있습니다.

Network Reachability

NetworkReachabilityManager는 Cellular와 와이파이 네트워크 인터페이스 모두에서 호스트와 주소의 도달가능성에 대한 변화를 감지할 수 있습니다.

let manager = NetworkReachabilityManager(host: "www.apple.com")

manager?.startListening { status in
    print("Network Status Changed: \(status)")
}

위 예시처럼 manager를 유지해야함을 기억해야 합니다. 그렇지 않으면 상태의 변화가 보고되지 않습니다.
또한, host string에 scheme을 포함하지 않아야 합니다. 그렇지 않으면 제대로 작동하지 않습니다.

네트워크 도달가능성을 사용할 때 어떤 것이 결정되어야 하는지에 대한 기억해야 할 것이 몇 가지 있습니다.

  • 네트워크 요청을 보내야 하는지에 대한 결정을 도달가능성으로 결정하지 않아야 합니다.
    • 항상 이것을 보내야만 합니다.
  • 도달가능성이 저장될 때, 네트워크 request에 실패한 것에 대해 retry하기 위해서 이벤트를 사용해야 합니다.
    • 네트워크 request들이 실패할지라도 request들을 retry하기에 좋은 시점입니다.
  • 네트워크 도달가능성 상태는 왜 네트워크 request가 실패할 수 있는지를 알아내는 데 유용할 수 있습니다.
    • 만약 네트워크 요청이 실패하면, 사용자에게 "request timed out"과 같은 기술적 error를 알려주는 것보다 오프라인 상태이기 때문에 네트워크 요청이 실패했다고 알려주는 것이 더 유용합니다.

네트워크에 실패한 request들에 도달가능성으로 retry를 시도하는 것 대신, RetryPolicy처럼 Alamofire에 내장된 RequestRetrier를 사용하는 것이 더 간단하고 신뢰할 수 있는 방법일 것입니다. default로 RetryPolicy는 오프라인 네트워크 연결을 포함한 다양한 error 조건들에 멱등성 request들을 retry할 것입니다.

0개의 댓글