충전했’오' 프로젝트에서 Access Token 뿐만이 아닌 Refresh Token을 도입하여 Authorization을 진행 및 Time-Out에 대한 처리에 대해 Research를 진행하던 도중 Alamofire에서 제공하는 **RequestInterceptor
, Retrier
를 이용하기 위해 이 게시물을 작성합니다
(Alamofire - Advanced Usage 를 기반으로 작성하였음)
**RequestInterceptor
, Retrier
를 살펴보기 전 Session이라는 개념을 먼저 살펴 볼 필요가 있습니다. 보통 Alamofire에서 request를 생성할 때 아래와 같은 코드를 많이 작성합니다
AF.request("https://httpbin.org/get")
AF는 Session의 Singleton instance인 default의 top-level name space로 위 코드는 아래의 코드와 같은 의미를 나타냅니다.
let session = Session.default
session.request("https://httpbin.org/get")
그렇다면 Session을 사용하는 경우는 언제일까요?!
URLSession
의 configuration
을 default
가 아닌 다른 값으로 설정하는 경우let configuration = URLSessionConfiguration.af.default
configuration.allowsCellularAccess = false
let session = Session(configuration: configuration)
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)
let delegate = SessionDelegate(fileManager: .default)
...
위 예시처럼 custom한 request를 생성하고자 원한다면 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] = [])
RequestInterceptor는 RequestAdaptor와 RequestRetrier의 성격을 가지고 있는 protocol입니다.
adapt method는 request 전 특정 작업을 먼저 수행이 가능합니다
아래 code는 oAuth 인증 시스템을 구현 시 header에 bearer token를 추가하는 과정입니다
func adapt(_ urlRequest: URLRequest, for _: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
guard let accessToken = KeychainWrapper.standard.string(forKey: "accessToken") else {
completion(.success(urlRequest))
return
}
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
completion(.success(urlRequest))
}
retry method는 api response에 대해 validation method를 통해 error로 분류된 response에 대해 retry여부를 결정합니다.
아래 code는 13(timeout), 401(unauthorized)에 대해 재시도를 수행하는 code입니다
13(timeout) : 2회 재시도
401(unauthorized): refresh token으로 access token 재발급
api 호출에서 공통적으로 처리 가능한 오류에 대해 retry method에서 처리 또한 가능할 것으로 보입니다.
func retry(_ request: Request, for _: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
guard let response = request.task?.response as? HTTPURLResponse else {
completion(.doNotRetryWithError(error))
return
}
switch response.statusCode {
case 13: // timeout
if request.retryCount < limit {
completion(.retry)
} else {
Singleton.shared.presentToastAlert.onNext(.network)
completion(.doNotRetry)
}
case 401: // unauthorized
AuthAPI.shared.refreshAuthentication()
.subscribe(onSuccess: { result in
switch result {
case let .success(data):
data.refreshAccessToken()
completion(.retry)
case let .failure(error):
Singleton.shared.unauthorized.onNext(())
}
})
.disposed(by: bag)
default:
completion(.doNotRetry)
}
}
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)
}