본문 바로가기

iOS

[Swift] Alamofire를 Moya처럼 사용해보자! By Router Pattern (6편 - ParameterEncoding을 통해 여러 Encoding 방식 대응하기)

[Swift] Alamofire를 Moya처럼 사용해보자! By Router Pattern (1편 - Foundation Setting)

[Swift] Alamofire를 Moya처럼 사용해보자! By Router Pattern (2편 - Services, Routers 구현)

[Swift] Alamofire를 Moya처럼 사용해보자! By Router Pattern (3편 - body, queryBody, requestPlain, Multipart 구현)

[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 통신을 하는 방식에 대해 다루겠습니다.