[Swift] Alamofire를 Moya처럼 사용해보자! By Router Pattern (1편 - Foundation Setting)
[Swift] Alamofire를 Moya처럼 사용해보자! By Router Pattern (2편 - Services, Routers 구현)
[Swift] Alamofire를 Moya처럼 사용해보자! By Router Pattern (4편 - EventLogger로 통신 결과 확인하기)
[Swift] Alamofire를 Moya처럼 사용해보자! By Router Pattern (5편 - Interceptor를 통해 네트워크 에러 및 토큰 갱신 처리하기)
4, 5편에서는 EventMonitor 및 Interceptor를 통해 Request 및 Response에 대해 전처리, 후처리 하는 방식을 알아 보았습니다.
이번 편에서는 ParameterEncoding Protocol에 대해 알아보고, 이를 활용해 여러 케이스의 Encoding 방식에 대해 대응할 수 있도록 Router를 개편하겠습니다.
ParameterEncoding 프로토콜
Alamofire에는 Encoder의 역할과 비슷하게 특정 Request에 원하는 파라미터를 원하는 방식으로 encode 할 수 있도록 돕는 명세를 제공하는 ParameterEncoding 프로토콜이 존재합니다.
/// A type used to define how a set of parameters are applied to a `URLRequest`.
public protocol ParameterEncoding {
/// Creates a `URLRequest` by encoding parameters and applying them on the passed request.
///
/// - Parameters:
/// - urlRequest: `URLRequestConvertible` value onto which parameters will be encoded.
/// - parameters: `Parameters` to encode onto the request.
///
/// - Returns: The encoded `URLRequest`.
/// - Throws: Any `Error` produced during parameter encoding.
func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest
}
위 프로토콜을 이용하면 URLRequest에 적절한 방식으로 parameter를 인코딩할 수 있게 하는 Encoder를 구현할 수 있습니다.
URLEncoding : destination, arrayEncoding, boolEncoding 파라미터를 통해 URL을 Encoding 하는 방식을 세부적으로 결정하자!
/// Creates a url-encoded query string to be set as or appended to any existing URL query string or set as the HTTP
/// body of the URL request. Whether the query string is set or appended to any existing URL query string or set as
/// the HTTP body depends on the destination of the encoding.
///
/// The `Content-Type` HTTP header field of an encoded request with HTTP body is set to
/// `application/x-www-form-urlencoded; charset=utf-8`.
///
/// There is no published specification for how to encode collection types. By default the convention of appending
/// `[]` to the key for array values (`foo[]=1&foo[]=2`), and appending the key surrounded by square brackets for
/// nested dictionary values (`foo[bar]=baz`) is used. Optionally, `ArrayEncoding` can be used to omit the
/// square brackets appended to array keys.
///
/// `BoolEncoding` can be used to configure how boolean values are encoded. The default behavior is to encode
/// `true` as 1 and `false` as 0.
public struct URLEncoding: ParameterEncoding { }
먼저 ParameterEncoding의 구현체 중 하나인 URLEncoding입니다. 주요 사용 방식은 array를 인코딩할 때에, bracket의 포함 여부를 결정할 수 있도록 해줍니다. 또한 BoolEncoding이라는 파라미터를 제공하여 Bool 타입에 대해서도 편리하게 Encoding할 수 있도록 해주네요!
그리고 아래 메서드는 URLEncoding 클래스에서 제공하는 public method 입니다. dictionary 형태로 들어온 파라미터에 대해 재귀적으로 encoding해주는 방식이 인상적이네요!
public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
var components: [(String, String)] = []
switch value {
case let dictionary as [String: Any]:
for (nestedKey, value) in dictionary {
components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
}
case let array as [Any]:
for (index, value) in array.enumerated() {
components += queryComponents(fromKey: arrayEncoding.encode(key: key, atIndex: index), value: value)
}
case let number as NSNumber:
if number.isBool {
components.append((escape(key), escape(boolEncoding.encode(value: number.boolValue))))
} else {
components.append((escape(key), escape("\(number)")))
}
case let bool as Bool:
components.append((escape(key), escape(boolEncoding.encode(value: bool))))
default:
components.append((escape(key), escape("\(value)")))
}
return components
}
JsonEncoding : JsonSeriallization을 이용하여 Json 형식으로 Encoding 하자!
/// Uses `JSONSerialization` to create a JSON representation of the parameters object, which is set as the body of the
/// request. The `Content-Type` HTTP header field of an encoded request is set to `application/json`.
public struct JSONEncoding: ParameterEncoding {
JsonEncoding은 URLEncoding과 다르게 body에 대한 Encoding만 가능합니다. Json 형식이 body에만 쓰이기 때문이죠!
따라서 query를 URL에 encoding하게 위해선 앞에 나온 URLEncoding을 이용해야합니다! 자동을 header의 type이 application/json으로 바뀐다고도 하네요!
아래의 code에서 JsonEncoding의 역할을 알 수 있습니다!
public func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()
guard let parameters = parameters else { return urlRequest }
guard JSONSerialization.isValidJSONObject(parameters) else {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: Error.invalidJSONObject))
}
do {
let data = try JSONSerialization.data(withJSONObject: parameters, options: options)
if urlRequest.headers["Content-Type"] == nil {
urlRequest.headers.update(.contentType("application/json"))
}
urlRequest.httpBody = data
} catch {
throw AFError.parameterEncodingFailed(reason: .jsonEncodingFailed(error: error))
}
return urlRequest
}
ParameterEncoding을 통해 다양한 Encoding에 대응할 수 있도록 BaseRouter 개선하기!
Alamofire가 이렇게 유용한 Encoding Protocol을 제공하니, 이제 잘 활용할 수 있는 방향으로 Router를 개선해봅시다.
먼저 아래처럼 parameterEncoding 프로퍼티를 추가합니다.
public protocol BaseRouter: URLRequestConvertible {
var baseURL: String { get }
var method: HTTPMethod { get }
var path: String { get }
var parameters: RequestParams { get }
var header: HeaderType { get }
var multipart: MultipartFormData { get }
var parameterEncoding: ParameterEncoding { get }
}
그리고 RequestParams에 아래와 같이 parameterEncoding에 관한 연관값들을 추가해주고, makeParameterForRequest에서 케이스들에 대해 핸들링해 줍니다. ParameterEncoding이 encode 메서드를 제공하기 때문에, 아래와 같이 단순하게 encode 메서드를 호출만 해주면 됩니다!
public enum RequestParams {
case queryBody(_ query: [String: Any], _ body: [String: Any], parameterEncoding: ParameterEncoding = URLEncoding(), bodyEncoding: ParameterEncoding = JSONEncoding.default)
case query(_ query: [String: Any], parameterEncoding: ParameterEncoding = URLEncoding())
case requestBody(_ body: [String: Any], bodyEncoding: ParameterEncoding = JSONEncoding.default)
case requestPlain
}
private func makeParameterForRequest(to request: URLRequest, with url: URL) throws -> URLRequest {
var request = request
switch parameters {
case .query(let query, let parameterEncoding):
request = try parameterEncoding.encode(request, with: query)
case .requestBody(let body, let parameterEncoding):
request = try parameterEncoding.encode(request, with: body)
case .queryBody(let query, let body, let parameterEncoding, let bodyEncoding):
request = try parameterEncoding.encode(request, with: query)
request = try bodyEncoding.encode(request, with: body)
case .requestPlain:
break
}
return request
}
마지막으로 BaseRouter의 구현체에서는 아래와 같이 ParameterEncoding을 사용할 수 있습니다.
var parameters: RequestParams {
switch self {
case .uploadVoice(let id):
let query: [String: Any] = [
"_id": id
]
return .query(query, parameterEncoding: parameterEncoding)
default: return .requestPlain
}
}
var parameterEncoding: ParameterEncoding {
switch self {
case .uploadVoice:
return URLEncoding.init(destination: .queryString, arrayEncoding: .noBrackets, boolEncoding: .numeric)
default:
return JSONEncoding.default
}
}
여기까지 하면 각 API의 케이스에 대해 편하게 Encoding 작업을 할 수 있습니다.
다음 편에서는 Alamofire에서 Mock 통신을 하는 방식에 대해 다루겠습니다.