-
[iOS] Alamofire의 고급 사용법!! SessionManager, SessionDelegate, RequestAdapter, RequestRetrier앱등이에게 살충제를 뿌린다./iOS 2018. 1. 16. 21:21
Alamofire - AdvancedUsage
제가 개발중인 프로젝트에서는 중요하다고 판단되는 API의 경우, API가 실패하면 3회 재시도하는 로직이 있습니다.
이러한 로직을 공통으로 처리하고자 하는 도중,
Alamofire에서 제공하는 Retrier는 뭐지?
라는 의문을 시작으로 이 문서를 작성한다.(Alamofire - Advanced Usage를 기반으로 작성하였음)
Session Manager
Retrier
를 알아보기 전에SessionManager
라는 클래스를 짚고 넘어갈 필요가 있다. 우리는 보통 Alamofire의 request를 생성할 때, 아래와 같은 코드를 작성한다.Alamofire.request("http://www.naver.com")
사실
Alamofire.request()
는Alamofire.SessionManager
가 Default로 생성한 request이다. 따라서, 위에서 생성한 request는 이렇게 생성하는 것과도 같다.let sessionManager = Alamofire.SessionManager.default sessionManager.request("http://www.naver.com")
그렇다면 굳이
SessionManager
를 사용하는 경우는 언제인가?아래처럼, 디폴트가 아닌 커스텀한 request를 생성하고 싶을 때,
SessionManager
를 사용하면 된다.URLSession
의configuration
을default
가 아닌 다른 값으로 설정하거나,- Background Session을 만든다던가,
- 디폴트 헤더를 넣어주길 원한다던가
물론
Alamofire.request()
로 생성한 request에 커스텀한 옵션을 추가해도 된다.아래는
SessionManager
를 사용하여 request를 생성하는 예제 코드다.Default Configuration으로 Session Manager 생성하기
// Default Session: 기본적인 Session으로 디스크 기반 캐싱을 지원합니다. let configuration = URLSessionConfiguration.default let sessionManager = Alamofire.SessionManager(configuration: configuration)
Background Configuration으로 Session Manager 생성하기
// Background Session: 앱이 종료된 이후에도 통신이 이뤄지는 것을 지원하는 세션입니다. let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background") let sessionManager = Alamofire.SessionManager(configuration: configuration)
Ephemeral Configuration으로 Session Manager 생성하기
// Ephemeral Session: 어떠한 데이터도 저장하지 않는 형태의 세션입니다. (1회성 리퀘스트) let configuration = URLSessionConfiguration.ephemeral let sessionManager = Alamofire.SessionManager(configuration: configuration)
디폴트 헤더 설정하기
var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders defaultHeaders["DNT"] = "1 (Do Not Track Enabled)" let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = defaultHeaders let sessionManager = Alamofire.SessionManager(configuration: configuration)
디폴트가 아닌 옵션을 적용한다는 것이 포인트이니, Background가 뭔지 Ephemeral이 뭔지는 몰라도 무방합니다.
SessionDelegate
SessionManager
는SessionDelegate
타입의 프로퍼티를 갖고 있다.SessionDelegate
는URLSession
에서 발생하는 콜백함수들을 처리하기 위한 Delegate역할을 하는 객체이고,SessionManager
가 디폴트로 생성하여 오너쉽을 갖는다./// SessionManager의 생성자 public init( configuration: URLSessionConfiguration = URLSessionConfiguration.default, delegate: SessionDelegate = SessionDelegate(), serverTrustPolicyManager: ServerTrustPolicyManager? = nil) { self.delegate = delegate self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil) commonInit(serverTrustPolicyManager: serverTrustPolicyManager) }
SessionManager와 SessionDelegate요약
- Alamofire는
Foundation
프레임워크의URLSession
을 기반으로 네트워킹기능을 제공하고 있다. SessionManager
로 request를 생성하면 커스텀한 request를 생성하기 편하다.Alamofire.request(:_)
로 생성한 request는SessionManager
에서default
로 생성한 request와 같다.SessionDelegate
는URLSession
에서 발생하는 콜백함수들을 처리하기 위한 Delegate역할을 하는 객체이고,SessionManager
가 디폴트로 생성하여 오너쉽을 갖는다.
Adapting and Retrying Requests
Alamofire에서는
RequestAdapter
와RequestRetrier
를 설명하면서 OAuth를 구현하는 상황을 가정하고 있습니다.예제코드를 볼 때, 참고 바랍니다.
RequestAdapter
Retrier는 이름에서 감이 온다. 하지만 Adapter는 무엇일까? 결론부터 말하면 Adapter는 딱히 필요 없다고 보여진다.
SessionManager
는RequestAdapter
타입의 프로퍼티를 갖고 있다.public protocol RequestAdapter { /// Inspects and adapts the specified `URLRequest` in some manner if necessary and returns the result. /// (파라미터로 전달받은 urlReqeust를 적절히 수정하여 리턴한다.) func adapt(_ urlRequest: URLRequest) throws -> URLRequest } open class SessionManager { /// The request adapter called each time a new request is created. open var adapter: RequestAdapter? ... }
그리고 request를 생성할 때마다, 이
adapter
를 사용한다.adapter
는 request를 생성하기 전에, request에 원하는 작업을 수행시키는 역할을 한다. 즉, 개발자가 원하는 request 커스터마이징이 진행된다.원하는 헤더가 삽입된 request가 생성된다.
class AccessTokenAdapter: RequestAdapter { private let accessToken: String init(accessToken: String) { self.accessToken = accessToken } /// "https://httpbin.org"로 요청하는 리퀘스트는 헤더필드에 "Authorization"이 추가된다. func adapt(_ urlRequest: URLRequest) throws -> URLRequest { var urlRequest = urlRequest if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://httpbin.org") { urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") } return urlRequest } }
let sessionManager = SessionManager() sessionManager.adapter = AccessTokenAdapter(accessToken: "1234") sessionManager.request("https://httpbin.org/get")
RequestRetrier
RequestRetrier
프로토콜은 Request에서 Error가 발생했을 때, 재시도할 수 있는 기능을 제공한다.public protocol RequestRetrier { /// Determines whether the `Request` should be retried by calling the `completion` closure. /// /// This operation is fully asychronous. Any amount of time can be taken to determine whether the request needs /// to be retried. The one requirement is that the completion closure is called to ensure the request is properly cleaned up after. /// /// completion 클로져에서 Request를 재시도할 지 말지 지정할 수 있다. /// /// 이 메소드는 비동기로 작동한다. 따라서, Request를 재시도할 지 말지 판단할 때, 많은 시간을 써도 문제없다. /// 다만, Request가 완료되었음을 알려주기 위해 completion 클로져는 반드시 실행되어야 한다. func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) } open class SessionManager { /// The request retrier called each time a request encounters an error to determine whether to retry the request. open var retrier: RequestRetrier? { get { return delegate.retrier } set { delegate.retrier = newValue } } ... }
Ex)
RequestAdapter
와RequestRetrier
를 이용하여OAuth
를 구현하기.class OAuth2Handler: RequestAdapter, RequestRetrier { private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void private let sessionManager: SessionManager = { let configuration = URLSessionConfiguration.default configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders return SessionManager(configuration: configuration) }() private let lock = NSLock() private var clientID: String private var baseURLString: String private var accessToken: String private var refreshToken: String private var isRefreshing = false private var requestsToRetry: [RequestRetryCompletion] = [] // MARK: - Initialization public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) { self.clientID = clientID self.baseURLString = baseURLString self.accessToken = accessToken self.refreshToken = refreshToken } // MARK: - RequestAdapter func adapt(_ urlRequest: URLRequest) throws -> URLRequest { if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) { var urlRequest = urlRequest urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization") return urlRequest } return urlRequest } // MARK: - RequestRetrier func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) { lock.lock() ; defer { lock.unlock() } if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 { requestsToRetry.append(completion) if !isRefreshing { refreshTokens { [weak self] succeeded, accessToken, refreshToken in guard let strongSelf = self else { return } strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() } if let accessToken = accessToken, let refreshToken = refreshToken { strongSelf.accessToken = accessToken strongSelf.refreshToken = refreshToken } strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) } strongSelf.requestsToRetry.removeAll() } } } else { completion(false, 0.0) } } // MARK: - Private - Refresh Tokens private func refreshTokens(completion: @escaping RefreshCompletion) { guard !isRefreshing else { return } isRefreshing = true let urlString = "\(baseURLString)/oauth2/token" let parameters: [String: Any] = [ "access_token": accessToken, "refresh_token": refreshToken, "client_id": clientID, "grant_type": "refresh_token" ] sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default) .responseJSON { [weak self] response in guard let strongSelf = self else { return } if let json = response.result.value as? [String: Any], let accessToken = json["access_token"] as? String, let refreshToken = json["refresh_token"] as? String { completion(true, accessToken, refreshToken) } else { completion(false, nil, nil) } strongSelf.isRefreshing = false } } }
let baseURLString = "https://some.domain-behind-oauth2.com" let oauthHandler = OAuth2Handler( clientID: "12345678", baseURLString: baseURLString, accessToken: "abcd1234", refreshToken: "ef56789a" ) let sessionManager = SessionManager() sessionManager.adapter = oauthHandler sessionManager.retrier = oauthHandler let urlString = "\(baseURLString)/some/endpoint" sessionManager.request(urlString).validate().responseJSON { response in debugPrint(response) }
OAuth2Handler
는SessionManager
에adapter
와retrier
를 모두 제공하고 있다.그 결과 API에 필요한
access token
이 잘못된 경우, 자동으로access token
을 재발급하는 기능이 구현되었다.개인적 의견
Alamofire에서 Retry를 하는 코드를 살펴보면
URLSessionTaskDelegate
의 콜백메소드인urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
에서 호출된다. 즉,RequestRetrier
는 HTTP 통신의 결과에서 에러를 검출되어야 작동하는 것이다.API를 사용하는 환경에서 발생하는 예외케이스는 몇 가지가 있겠다.
- HTTP통신 자체에서 에러 발생하는 경우. Ex) 500, 404 등
- HTTP통신은 성공적이지만, 기대하는 결과값이 아닌 경우. Ex) 좋아요 어뷰징 방지, 삭제된 글 접근 등
1의 경우는
RequestAdapter
와RequestRetrier
를 통해 해결이 가능한 반면, 2의 경우는 HTTP 통신 자체는 성공적이었으므로,RequestRetrier
가 반응하지 않을 것이다.따라서,
RequestAdapter
와RequestRetrier
를 적절히 사용한다면, API를 호출하는 주요로직에서 코드를 분리할 수 있는 장점이 있다고 판단된다.하지만, 2와 같은 예외케이스를
didCompleteWithError
메소드에서 걸러지지 않기 때문에, 쓰지 않는편이 좋겠다.'앱등이에게 살충제를 뿌린다. > iOS' 카테고리의 다른 글