관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] 2. Alamofire 사용 방법 - 토큰 갱신 방법1 (Interceptor, adapt, retry) 본문

iOS 응용 (swift)

[iOS - swift] 2. Alamofire 사용 방법 - 토큰 갱신 방법1 (Interceptor, adapt, retry)

jake-kim 2021. 10. 16. 02:43

1. Alamofire 사용 방법 - Network Layer 구현 (Moya 프레임워크처럼 사용하는 방법)

2. Alamofire 사용 방법 - 토큰 갱신 방법1 (Interceptor, adapt, retry)

3. Alamofire 사용 방법 - 토큰 갱신 방법2 (AuthenticationCredential, Authenticator, AuthenticationInterceptor)

4. Alamofire 사용 방법 - 로그 Log (EventMonitor)


Interceptor란?

  • 서버에 요청을 보내기 전에, 중간에 가로채서 어떤 작업을 한 뒤 다시 서버로 보내는 역할
  • Alamofire를 사용하면 RequestInterceptor 프로토콜을 준수한 클래스의 인스턴스를 request에 실어서 보내면 동작
    • adapt(): API 호출 전에 urlRequest에 관한 처리를 가로채서 적용하는 메서드
    • retry(): API 호출 결과로 Error가 발생한 경우, 처리한 다음에 Error가 발생한 API를 다시 호출할것인지 적용하는 메서드

RequestInterceptor

  • RequestInterceptor: RequestAdaptor와 RequestRetrier의 성격을 가지고 있는 프로토콜

RequestAdaptor

  • adapt 메서드: request전에 특정 작업을 하고싶은 경우 사용

  • 예시) request전에 header에 bearer token 값을 세팅
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
    • retry 메서드: 요청을 재시도해야하는지 여부를 결정하는 메소드

  • completion으로 RetryResult를 넘기며, 재시도하는 유형에 대한 값

  • 예시) 특정 오류가 발생한 경우, retry가 필요한 경우 delay값을 주고 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)
    }
}

예제)

  • API호출 시 adapt에서 가로채서 urlRequest에 accessToken을 추가한 urlRequest으로 다시 반환
    • RequestInterceptor를 준수하여 adapt메서드에서 구현
import Alamofire

final class MyRequestInterceptor: RequestInterceptor {
    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        guard urlRequest.url?.absoluteString.hasPrefix("https://api.agify.io") == true,
              let accessToken = KeychainServiceImpl.shared.accessToken else {
                  completion(.success(urlRequest))
                  return
              }

        var urlRequest = urlRequest
        urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
        completion(.success(urlRequest))
    }
}
  • accessToken을 가지고 요청했을 때 401에러가 떨어지는 경우, 토큰을 refresh시키는 로직
    • retry 메서드에서 구현
    • 이전 API에 대해서 retry가 필요한 경우, completion(.retry)를하고 필요하지 않은 경우 completion(.doNotRetryWithError(error) 실행
    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
            completion(.doNotRetryWithError(error))
            return
        }

        RefreshTokenAPI.refreshToken { result in
            switch result {
            case .success(let accessToken):
                KeychainServiceImpl.shared.accessToken = accessToken
                completion(.retry)
            case .failure(let error):
                completion(.doNotRetryWithError(error))
            }
        }
    }
  • 전체 코드
import Alamofire

final class MyRequestInterceptor: RequestInterceptor {
    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        guard urlRequest.url?.absoluteString.hasPrefix("https://api.agify.io") == true,
              let accessToken = KeychainServiceImpl.shared.accessToken else {
                  completion(.success(urlRequest))
                  return
              }

        var urlRequest = urlRequest
        urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
        completion(.success(urlRequest))
    }

    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
            completion(.doNotRetryWithError(error))
            return
        }

        /* TODO
        RefreshTokenAPI.refreshToken { result in
            switch result {
            case .success(let accessToken):
                KeychainServiceImpl.shared.accessToken = accessToken
                completion(.retry)
            case .failure(let error):
                completion(.doNotRetryWithError(error))
            }
        }
        */
    }
}
  • 사용하는 쪽 - AF.request(_:interceptor)에서 interceptor에 객체 삽입

* 전체 소스코드: https://github.com/JK0369/ExAlamofire

 

* 참고

https://github.com/Alamofire/Alamofire/blob/master/Documentation/AdvancedUsage.md#adapting-and-retrying-requests-with-requestinterceptor

Comments