Alamofire는 URLSession을 쉽게 사용하게 해 주고 statusCode에 따른 응답 유효성 검사, 응답값 디코딩 등등 여러 가지 편의 기능들을 여럿 지원해 준다.
대표적인 기능으로는 요청을 보낼때마다 공통적인 부분 혹은 로직을 분리해서 재사용할 수 있게 해주는 Interceptor, 요청을 보낼 때 어떻게 작동하고 있는지 Console의 출력을 통해 보여주는 EventMonitor 기능등이 있다.
이러한 기능들은 쉽게 사용할 수 있도록 기본적인 구현이 제공된다. Alamofire에서 기본적으로 구현된것만으로 많은 오픈 API를 대응할 수 있다. 구글링을 통해 Alamofire를 사용하는 코드를 보면 대부분이 이 코드 혹은 이 코드에서 크게 벗어나지 않을 것이다.
AF.request("https://www.naver.com").validate().responseDecodable(of:RegisterUserResponseDTO.self) { response in
switch response.result {
case .success(let success):
//do somthing...
case .failure(let failure):
//do somthing...
}
}
다른 코드가 있다고 한다면 validate에 Range를 지정해준다던가, Interceptor를 request에 넣는 정도..
하지만 기본적인 구현으로는 대응 할 수 없는 상황이 존재한다.
예를 들면 이번 새싹에서 진행하는 SLP 프로젝트에서 제공되는 서버의 응답 에러는 다음과 같다.
Status Code | 에러 원인 | 에러 body값 |
400 | 알맞지 않은 서버 키 값 | "errorCode": "1" |
400 | 옳바르지 않는 경로 | "errorCode": "2" |
400 | Access Token 시간 만료 | "errorCode": "3" |
Status Code가 모두 동일한 상황이다. 이 상황에서 응답 에러를 구분하려면 Data를 디코딩해서 errorCode로 구분해야 한다.
하지만 기본적으로 제공되는 validate와 Interceptor에서는 이를 구분 할 수가 없다.
validate()는 status Code로 유효성을 검사하고, Interceptor에서는 오류를 받았을 때 해당 Interceptor를 트리거한 에러와 Request만을 인자값으로 제공한다.
//RequestInterceptor
func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
}
만약 엑세스 토큰이 만료되어서 Interceptor에서 토큰을 재발급받고 retry를 하려고 한다면, 응답 에러가 액세스 토큰이 만료된 응답 에러일 때 이를 수행해야 하는데, 여기서는 오류를 디코딩할 수 없으므로 액세스 토큰이 만료되었을 때만 액세스 토큰을 재발급할 수 없다.
물론 이런 방식으로도 위 문제를 해결 할 수 있다.
AF.request("https://www.naver.com").validate(statusCode: 200...500).responseDecodable { response in
if response.response?.statusCode != 200 {
let errorModel = JSONDecoder().decode(ErrorModel.self, from: response.data)
//do something..
} else {
//do something..
}
}
validate를 200부터 500까지 모두 유효하다고 하고 난 후 response에서 이를 처리하는 방식이다.
하지만 이 방식으로는 interceptor가 실행되지도 않고, Alamofire를 사용하는 이유도 없어진다고 생각한다. URLSession으로 응답값을 받아서 하는 방식과 차이점이 없다.
이런 문제점을 해결하기위해 Alamofire는 유효성 검사 및 에러 핸들링하는 방식을 2가지 제시하고 있다.
- Custom Validate
- Custom ResponseSerializer
첫 번째 방식은 Validate를 커스텀하는 방식이고, 두 번째 방식은 ResponseSerializer를 커스텀하는 방식이다.
이 두가지 방식의 차이점을 알기 위해서는 우선 Alamofire가 어떤 흐름으로 네트워크 요청을 하고, 응답값을 처리하는지를 알아야 한다.
Alamofire의 Flow
대표적인 Alamofire의 사용 코드로 예시를 들겠다. 이 코드는 다음과 같은 과정을 통해 최종적으로 후행 클로저를 실행하게 된다.
AF.request("https://www.naver.com").validate().responseDecodable(of:RegisterUserResponseDTO.self) { response in
switch response.result {
case .success(let success):
//do somthing...
case .failure(let failure):
//do somthing...
}
}
- AF.request를 실행하게 되면 전달받은 URLRequestConvertible 혹은 String값, Interceptor로 DataRequest란 객체를 생성
- Session에서 생성한 Datarequest를 perform함수의 매개변수에 넣고 실행
- 전달받은 URLRequestConvertible의 asURLRequest()를 호출하여 URLRequest 객체 생성
- adapter가 있을 경우 생성한 URLRequest를 사용하며, 수정된 URLRequest 전달
- 생성된 URLRequest로 URLSessionTask를 만들기 위해 DataRequest에 URLRequest를 전달
- DataRequest는 URLSession으로 URLRequest를 전달
- URLSession은 URLSessionDataTask를 생성한 후 DataRequest에 리턴
- DataRequest는 Session에게 받은 URLSessionDataTask를 리턴
- Session은 받은 URLSessionDataTask를 실행(resume)하여 요청
- 시간이 지나 요청에 대한 응답값이 온다면, SessionDelegate에서 이를 확인하고 DataTask의 didCompleteTask를 호출
- DataTask는 1차적으로 validate로 응답에 대한 유효성을 검사
- 응답이 유효하지 않는 경우 RequestRetrier를 호출. 에러가 아닌 경우 다음단계로 넘어감
- 해당 에러가 retry일 경우 SessionDelegate에게 재요청을 하겠다고 알림
- notRetry일 경우 responseSerializer를 실행 및 에러 전달
- ResponseSerializer에서 작업을 수행
- 작업을 수행중 에러가 발생하거나 validate로부터 응답이 아니고 에러를 받은 경우 RequestRetrier를 호출
- 해당 에러가 retry일 경우 SessionDelegate에게 재요청을 하겠다고 알림
- notRetry일 경우 에러 전달
- 에러가 아닌 경우 수행한 결과를 전달
이 과정을 시퀸스 다이어그램으로 표현하면 다음과 같다.
Validate
Flow를 보면 Alamofire는 우선 validate로 응답값의 유효성 검사를 수행한다. 여기서 말하는 validate는 위 코드에 있는 validate다.
기본적으로 제공되는 validate를 보면 다음과 같이 구현되어 있다.
public func validate() -> Self {
let contentTypes: () -> [String] = { [unowned self] in
acceptableContentTypes
}
return validate(statusCode: acceptableStatusCodes).validate(contentType: contentTypes())
}
acceptableStatusCodes는 기본적으로 제공되는 유효한 statusCode로 200..<300으로 되어 있고, contentType은 "*/*"으로 되어있다. "*/*" 모든 타입을 허용한다는 뜻이다.
그래서 기본적으로 제공되는 validate는 statusCode가 200..<300인 경우 유효한 응답이라고 처리한다.
따라서 우리는 이 validate를 이용해 응답값의 유효성을 처리 할 수 있다. 물론 Alamofire에서 이를 지원해 준다.
//Request
public typealias ValidationResult = Result<Void, Error>
public typealias Validation = (URLRequest?, HTTPURLResponse, Data?) -> ValidationResult
public func validate(_ validation: @escaping Validation) -> Self {
let validator: () -> Void = { [unowned self] in
guard error == nil, let response = response else { return }
let result = validation(request, response, data)
if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) }
eventMonitor?.request(self,
didValidateRequest: request,
response: response,
data: data,
withResult: result)
}
validators.write { $0.append(validator) }
return self
}
해당 함수의 validation 클로저로 유효성 검사를 위한 로직을 구현하여 추가할 수 있다.
extension DataRequest {
func slpValidate() -> Self {
validate { _, response, data in
if response.statusCode == 200 {
return .success(())
}
guard let data else { return .failure(//전달할 에러) }
do {
let errorCode = try JSONDecoder().decode(SLPErrorModel.self, from: data)
//errCode를 이용해 에러 매핑
return .failure(//매핑한 에러 전달)
} catch {
return .failure(//전달할 에러)
}
}
}
}
Response Serializer
두 번째 방법인 Response Serializer를 이용하는 방법이다.
ResponseSerializer이 뭔가 할 수 있는데, 우리는 늘 사용하고 있다. 위 코드에서는 바로 responseDecodable() 함수다. 해당 함수의 구현 부분에 들어가게 되면
@discardableResult
public func responseDecodable<T: Decodable>(of type: T.Type = T.self,
queue: DispatchQueue = .main,
dataPreprocessor: DataPreprocessor = DecodableResponseSerializer<T>.defaultDataPreprocessor,
decoder: DataDecoder = JSONDecoder(),
emptyResponseCodes: Set<Int> = DecodableResponseSerializer<T>.defaultEmptyResponseCodes,
emptyRequestMethods: Set<HTTPMethod> = DecodableResponseSerializer<T>.defaultEmptyRequestMethods,
completionHandler: @escaping (AFDataResponse<T>) -> Void) -> Self {
response(queue: queue,
responseSerializer: DecodableResponseSerializer(dataPreprocessor: dataPreprocessor,
decoder: decoder,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
}
이 함수는 DecodableResponseSerialzer이라는 객체를 생성하여 사용하는 것 을 볼 수 있다.
Response Serializer는 요청의 응답값을 마지막에 처리해 전달을 하는 역할을 한다.
DecodableResponseSerialzer의 구현 부분은 그저 응답값을 디코딩하는 역할을 한다. 응답값을 디코딩하여 디코딩에 성공할 시 값을 전달하고, 실패할 시 실패했다는 오류를 발생시킨다. 그 외에도 data가 없는 경우 같은 로직도 처리하지만 해당 객체의 핵심은 응답값을 디코딩해 값을 전달을 해주는 역할을 한다.
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T {
guard error == nil else { throw error! }
guard var data = data, !data.isEmpty else {
guard emptyResponseAllowed(forRequest: request, response: response) else {
throw AFError.responseSerializationFailed(reason: .inputDataNilOrZeroLength)
}
guard let emptyResponseType = T.self as? EmptyResponse.Type, let emptyValue = emptyResponseType.emptyValue() as? T else {
throw AFError.responseSerializationFailed(reason: .invalidEmptyResponse(type: "\(T.self)"))
}
return emptyValue
}
data = try dataPreprocessor.preprocess(data)
do {
return try decoder.decode(T.self, from: data)
} catch {
throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error))
}
}
이를 이용해 이번 slp의 유효성을 검증하는 로직을 만든다면 다음과 같이 만들어 줄 수 있다.
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> T {
guard error == nil, let response else { throw error! }
guard let data, !data.isEmpty else {
return try responseEmpty(request: request, response: response)
}
if !acceptableStatusCode.contains(response.statusCode) {
let errorCode = try decoder.decode(SLPErrorModel.self, from: data).errorCode
try mappingError(errorCode: errorCode)
}
do {
return try decoder.decode(T.self, from: data)
} catch {
throw AFError.responseSerializationFailed(reason: .decodingFailed(error: error))
}
}
언제 Validate를 사용하고, 언제 Response Serializer를 사용할까?
이 글을 보다 보면 자연스럽게 드는 의문이 "그렇다면 validate와 Response Serializer의 차이는 무엇이고, 언제 어떤 걸 사용해야 할까?"라는 생각이 들것이다.
우선은 Validate는 응답이 왔을 때 처음 실행된다. 그 후 RequestRetrier를 호출한다.
반면에 Response Serializer는 validate로부터 받은 값을 처리한 후 RequestRetrier를 호출한다.
Validate에서 응답값을 처리했을 때 에러가 발생하면 RequestRetrier이 1번 호출되게 되고 slp에서는 JSONDecoder를 생성한 후 디코딩 할 것이다. 그 후 Response Serializer가 값을 받아 또 에러를 받는다면 RequestRetrier이 또 호출되어 2번 호출되고 JSONDecoder를 생성하여 또 디코딩할 것이다. 이는 매우 비효율적이다. 따라서 Response Serializer를 이용하여 응답값의 유효성을 검사하는 것이 더 효율적이다.
비슷한 사례가 있나 싶어서 issue에서 검색해 보니 alamofire 만든 사람도 Response Serializer를 만들어서 사용한다고 한다. validate는 가벼운 유효성 검사를 하는 것을 권장하고 있다.
물론 상황마다 다 다르니까 반드시 Response Serializer를 사용하는 게 옳은 것만은 아니겠지만, 이러한 로직을 고려하여 코드를 짠다면 좀 더 효율적인 코드를 짤 수 있을 거라 생각한다.
'iOS > 앱 개발' 카테고리의 다른 글
OperationQueue을 이용한 UICollectionView 성능 개선 (0) | 2024.04.10 |
---|---|
API의 response status를 좀 더 구조적으로 핸들링 해보자 (0) | 2023.12.24 |
URLSession으로 multipart/form 방식으로 데이터 전송 (0) | 2023.12.17 |
권한 요청, 설정 이동을 추상화(protocol)를 이용하여 재사용 가능하게 사용하기 (1) | 2023.11.21 |
MVVM Inout패턴 + 네트워크 에러 핸들링 (0) | 2023.11.10 |