안녕하세요 ✋
iOS에서는 네트워크 통신시 Alamofire를 많이 사용합니다.
해당 포스팅에서는 Alamofire의 요청은 어떻게 이뤄지는 살펴보려고합니다.
이를 통해 URLSession만 사용했을 때와 어떤점이 다른지 살펴보겠습니다.
우선 Session과 Interceptor를 먼저 설명하겠습니다.
Session
Alamofire는 Session이라는 자체타입을 사용하여 네트워크 통신을 관리합니다.
흔히 요청을 보낼 때 사용하는 AF인스턴스도 Session타입의 인스턴스입니다.
Session타입은 내부적으로 URLSession인스턴스를 보유하고 있습니다.
Interceptor
Interceptor는 Alamofire를 사용해야하는 강력한 기능중 하나입니다.
인터셉터는 요청전 요청을 가로채어 추가적인 작업을 더해주거나
응답에 따라 요청을 다시 보내는 것과 같은 전후처리를 할 수 있습니다.
전자의 경우 Adapter, 후자의 경우 Retrier라고 불립니다.
Interceptor타입을 살펴보면, 두가지 타입을 다수 보유할 수 있습니다.
아래코드는 두타입을 각각 생성하고 Interceptor에 주입하는 코드입니다.
let tokenAdapter = Adapter { request, _, completion in
var newRequest = request
newRequest.headers.add(name: "Authorization", value: "Bearer \(accessToken)")
// 요청 마저 실행
completion(.success(newRequest))
}
let tokenRetrier = Retrier { request, _, error, completion in
print(error.localizedDescription)
// 해당 클로저는 리스폰스를 처리하기 전에 호출된다.
if let statusCode = request.response?.statusCode, statusCode == 401 {
print("Authorization실패, 토큰이 만료된 경우")
// 토큰 재발급후
// 요청 재시도
completion(.retry)
}
// 이후에 reponse컴플션 호출
completion(.doNotRetry)
}
let interceptor = Interceptor.interceptor(
adapter: tokenAdapter,
retrier: tokenRetrier
)
tokenAdaptor는 요청을 실행하기 직전에 호출되어 요청에 토큰관련 헤더를 더하고 요청을 실행합니다.
tokenRetrier는 응답에 에러가 발새한 경우 호출되어 특정 작업이후(ex 토큰 재발급) 요청을 재시도할 수 있습니다.
두가지가 어떻게 요청과 응답에 적용되는지는 Request flow를 통해서 설명하겠습니다.
Request flow
Session타입을 이용하여 요청이 진행되는 방식을 예시를 통해서 살펴보겠습니다.
URL을 통해서 유저 데이터를 인메모리로 가져오는 DataTask상황을 가정하겠습니다.
먼저 URLRequest를 생성합니다.
※ AF는 ~Convertible을 통해 URL및 URLRequest를 추상화합니다. 상황에 맞게 여러 방식을 사용하면됩니다.
URLRequest는 요청정보를 저장하는 구조체로 해당 정보에는 헤더, 바디 데이터, URL등 모든 요청 구성 요소가 포함됩니다.
위에서 설명한 Interceptor와 URLRequest를 request매서드에 전달함으로써 요청을 시작합니다.
각각의 매서들을 통해서 어떻게 플로우가 진행되는지 알아보겠습니다.
request
request매서드는 가장많이 사용하는 요청의 형태를 요청할 수 있습니다.
요청에 따른 응답을 전달받고 해당 응답의 몸체를 Data인스턴스로 메모리에 올리는 작업을 수행합니다.
매개변수로 전달받은 정보를 사용하여 DataReqeust클래스의 인스턴스를 생성합니다.
AF는 네트워크 요청을 단위로 작업을 관리하기 위해 Request라는 추상클래스를 만들고
DataRequest, DownloadRequest등 상황에 맞는 하위 타입들을 사용합니다.
perform매서드를 사용하여 특정요청을 실행합니다.
이과정을 쭉 지나치다보면 performSetupOperations함수가 실행되는 것을 확인할 수 있습니다.
위 함수를 보면 adapter매서드를 사용하여 Request로 부터 어뎁터를 불러온 후
어뎁터의 adapt매서드를 호출하는 것을 확인할 수 있습니다.
아래함수를 보시면, session의 인터셉터와 Request를 모두 불러오거나 둘중 하나만 불러오는 것을 확인할 수 있습니다.
Interceptor는 RequestAdapter와 RequestRetrier의 혼합 프로토콜 RequestInterceptor를 conform함으로
인터셉터가 RequestAdapter로 타입 숨김(like 업스케일링)되어 전달되게 됩니다.
RequestAdaptor 프로토콜의 경우 adapt함수를 요구하고 Interceptor에는 아래와 같이 (오버로딩된)함수가 구현되어 있습니다.
따라서 adapter함수로 부터 반환받은 어뎁터가 호출하는 adapt함수는 위코드이며,
자신이 보유한 어뎁터들을 재귀적으로 순회하며 URLRequest인스턴스에 적용하는 것을 확인할 수 있습니다.
다시한번 tokenAdapter를 보면
작업을 완료한 이후에 completion으로 수정된 URLRequest를 전달하는 것을 확인할 수 있는데,
이는 위 코드에서 adapt함수에 전달되는 클로져로 자신의 작업(URLRequest수정)이후에 다른 어뎁터의 작업을 실행합니다.
let tokenAdapter = Adapter { request, _, completion in
var newRequest = request
newRequest.headers.add(name: "Authorization", value: "Bearer \(accessToken)")
completion(.success(newRequest))
}
모든 어뎁터를 성곡적으로 적용한 이후, 마지막으로 validate함수를 통해 검증을 진행합니다.
이때의 검증은 요청타입(URLReqeust)의 유효성을 검증합니다.
HTTP매서드가 GET인데 바디에 데이터가 존재하는지? 같은 형식 검사를 합니다.
그후 변경된 URLRequest를 didAdaptInitialRequest매서드를 통해 Request(처음 생성한 DataRequest)에 저장합니다.
Request는 다수의 URLRequest를 배열로 저장합니다.
AF는 레이스 컨디션 문제를 해결하기 위해 mutableState라는 타입을 통해 리소를 관리합니다.
저장된 URLRequest는 나중에 요청을 재시도할 경우 사용됩니다.
잠깐 먼저 살펴보자면
아래코드는 재시도 여부를 확인하는 코드인데, request.request로 요청을 가져옵니다.
해당 프로퍼티는 requests에 가장마지막에 추가된 요소, 실질적으로 현재 요청에 사용된 URLRequest인스턴스임을 알 수 있습니다.
요청 재시도시 어떤 요청에 대한 재시도인지 위 과정을 통해 인지할 수 있습니다.
다시돌아와서 URLRequest의 검증을 마쳤다면 요청에 맞는 Task가 만들어집니다.
현재 요청타입은 DataRequest임으로 URLSessionDataTask가 만들어지게 됩니다.
최종적으로 아래 매서드에 의해 Task가 실행되게 됩니다.
자 여기까지 요청이 어떻게 시작되는지에 대한 내용을 다루었습니다.
이후부터는 응답을 확인및 검증하고 요청 재시도가 어떻게 이뤄지는지 확인해 보겠습니다.
Response
Session타입은 인스턴스 프로퍼티로 SessionDelegate객체를 가지고 있습니다.
SessionDelagate는 URLSessionDeleage, URLSessionDataDelegate등 기본적으로 제공하는 위임자역할을 총괄합니다.
호출되는 매서드에 따라 요청을 처리해야 하는데, Session이 Request등 요청에 대한 정보를 가지고 있어
SessionStateProvider타입을 통해 Session으로부터 데이터를 전달받습니다. 헤당 프로퍼티에는 세션인스턴스가 할당됩니다.
순환참조를 막기위해 weak키워드가 선언된 것을 확인할 수 있습니다.
복잡하지만 Session에서 위임자 패턴은 위와같이 동작합니다.
빨간색 화살표는 자신이 할당된 프로퍼티를 의미합니다.
Response Validation
우선 응답이 도착된 경우 아래 매서드가 가장 먼저 호출되게됩니다.
그후 request매서드를 통해(stateProvider사용) 특정 테스크에 맵핑되는 Request타입을 가져오고
didRecieveResponse매서드를 호출합니다.
해당 매서드의 경우 요청에 아래 매서드를 통해서 클로저를 추가해준 경우 해당 클로전들이 호출되게 됩니다.
요청에 바디에 데이터가 포함되어 있는 경우 아래 매소드가 호출됩니다.
didRecieve는 전달받은 데이터를 Request인스턴스에 저장합니다.
요청이 완료된 이후에는 아래 매서드가 호출됩니다.
해당 메서드에서 Request의 아래함수가 호출되게 됩니다.
validator들을 순회하며 호출하는 것을 확인할 수 있습니다.
validator들은 요청의 유효성을 검증하고 에러를 발생시킵니다.
이때 에러가 발생하느냐 마느냐에 따라 Retrier가 호출될지 결정되게 되는 구조입니다.
아래코드는 요청에 Validation클로저를 추가하는 코드입니다.
Validation을 추가하는 영역에서, 실패시 Request에 에러를 할당하도록 함으로
Validation을 실행하는 것만으로 검증 실패여부를 알 수 있습니디.
validator들을 순회한 이후 최종적으로 retryOrFinish함수가 호출되게 됩니다.
Retry request
Request의 에러가 nil이 아닌 경우 retryResult를 호출하게 됩니다.
retryResult내에서 Adapter와 비슷한 방식으로 인터셉터 보유한 Retrier들을 순회합니다.
Retrier들은 어뎁터처럼 자신들만의 클로저를 가지고, 검증조건에 따라 재시도를 할것인지 말것인지 정할 수 있습니다.
특정 Retrier가 doNotRetry를 클로저에 전달하는 경우만 다음 Retrier가 호출되게됩니다.
특정 Retrier가 결과적으로 completion에 retry를 전달하면 retryRequest함수가 호출되게 됩니다.
해당 함수내에서 perform을 통해 요청이 재시도 되는 것을 확인할 수 있습니다.
responseDecodable의 경우 데이터 직렬화 실패시에도 Retrier가 호출될 수 있습니다.
여기가까지 요청 시작 Adapt, Retrier, Response에 이르는 전과정을 살펴보았습니다.
Alamofire가 편한줄은 알았지만 이렇게 깊게알아보니 유용한기능이 있었고 제가 아직 알지못하는 기능도 있을 것이라고 생각됩니다.
그래도 기본적인 요청 플로우는 익힐 수 있었던 학습이 되었습니다.
해당 학습을 통해 Interceptor개념을 충분히 Swift Concurrency에 적용할 수 있을 것이라고 판단됩니다.
다음 포스팅에는 해당 내용으로 글을 작성해 보겠습니다. ✋
'iOS공통' 카테고리의 다른 글
[RxSwift] self순환 참조에 대해 (0) | 2024.07.19 |
---|---|
[iOS공통] Coordinator패턴을 적용한 페이지 전환 구현 (0) | 2024.07.01 |
[Extension] Action Extension, AppGroups과 함께 사용해보기 (0) | 2024.03.27 |
[Swift] Combine 4편: Combine을 사용한 비동기 코드처리 (0) | 2023.09.05 |
[Swift] Combine 3편: subscriber동작방식 이해하기 (0) | 2023.09.03 |