-
Notifications
You must be signed in to change notification settings - Fork 0
Moya와 비슷한 API Manager 만들기
Moya의 아이디어는 아래의 그림으로 해결된다.
Moya는 URLSession을 추상화한 AlamoFire을 다시 추상화한 프레임워크로 Network Layer를 탬플릿화해서 재사용성을 높히고, 개발자가 request, response에 신경쓰도록 한다.
사실 Moya를 사용했어도 되었지만, Moya를 사용하지 않고, Moya 처럼 구현해보자는 취지, 또한 Moya의 모든 기능을 쓰지 않을 것인데 굳이 서드파티가 필요할까 라는 생각으로 Moya와 비슷한 MOMO 네트워크 레이어를 구축했다.
protocol APIable {
var contentType: ContentType { get }
var requestType: RequestType { get }
var encodingType: EncodingType {get}
var header: [String: String]? { get }
var url: String { get }
var param: [String: String?]? { get }
}
APIable
프로토콜을 채택하면, request에 필요한 정보를 작성해야한다. EncodingType에는 Momo에서 쓰이는 방식은 URLEncoding과 JSONEncoding이 존재한다. RequestType은 CRUD가 REST형태로 되어있다. ContentType에도 각각 contentType에 대한 header가 만들어져 있다.
enum EncodingType {
case URLEncoding
case JSONEncoding
}
enum RequestType: String {
case get = "GET"
case post = "POST"
case delete = "DELETE"
case patch = "PATCH"
case put = "PUT"
var method: String {
return self.rawValue
}
}
enum ContentType {
case multiPartForm
case jsonData
case HTMLform
case noBody
var description: String {
switch self {
case .multiPartForm:
return "multipart/form-data"
case .jsonData:
return "aplication/json"
case .HTMLform:
return "application/x-www-form-urlencoded"
case .noBody:
return ""
}
}
GetApi은 APIable
채택한 enum
이다. 각 case
는 모두 해당 APIable
에 선언되어있는 변수들이 구현되어야 한다. 또한 request에 필요한 param
은 모두 enum
의 associate value
로 받는다.
enum GetApi: APIable {
case loginGet(token: String)
case userGet(token: String)
case policyGet(token: String, keyword: String?, location: String?, category: Filter?, page: Int)
}
case에 맞게 APIable에 선언된 변수의 구현이 필요하다.
ContentType
var contentType: ContentType {
switch self {
case .loginGet, .userGet, .policyGet:
return .jsonData
default:
return .noBody
}
}
EncodingType
var encodingType: EncodingType {
switch self {
case .loginGet, .userGet:
return .JSONEncoding
case .policyGet:
return .URLEncoding
default:
return .JSONEncoding
}
}
requestType
var requestType: RequestType {
return .get
}
URL
var url: String {
switch self {
case .policyGet:
return makePathtoURL(path: "/policy")
case .loginGet:
return makePathtoURL(path: "/auth/login")
case .userGet:
return makePathtoURL(path: "/member")
}
}
param
var param: [String : String?]? {
switch self {
case .loginGet(_), .userGet(_):
return nil
case .policyGet(_, let keyword, let location, let category, let page):
return ["keyword": keyword, "location": location, "category": category?.rawValue, "page": String(page)]
default:
return nil
}
}
header
var header: [String : String]? {
switch self {
case .policyGet(let token, _, _, _, _), .loginGet(let token), .userGet(let token):
return [ "Authorization" : "Bearer \(token)"]
default:
return nil
}
}
NetworkManager 클래스에는 request함수가 존재한다. request 함수 내에서 위에서 선언했던 APIable 채택한 Enum case 중 하나를 넣어주면 그 정보를 토대로 request를 만들어 서버와 통신한다. 함수는 func request(apiModel: APIable, completion: @escaping URLSessionResult) -> URLSessionTaskProtocol?
이렇게 생겼다. APIable을 채택하는 객체를 받고, completion을 통해서 통신 이후에 하려는 동작을 구현해야한다. 그리고 return은 URLSessionTaskProtocol?
이다. URLSessionResult
은 typealias로서, typealias URLSessionResult = ((Result<Data, Error>) -> Void)
이다.
switch apiModel.encodingType {
case .URLEncoding: // urlEncoding.QueryString형식으로 url 만들기
guard let tempUrl = URL.URLEncodingType(parameters: apiModel.param, url: apiModel.url) else {
completion(.failure(NetworkError.invalidURL))
return nil
}
url = tempUrl
case .JSONEncoding: //JSONEncoding일때는 그냥 url만들기
guard let tempUrl = URL(string: apiModel.url) else {
completion(.failure(NetworkError.invalidURL))
return nil
}
url = tempUrl
default: // formdata 돌릴때 그냥 URL를 만들기
guard let tempUrl = URL(string: apiModel.url) else {
completion(.failure(NetworkError.invalidURL))
return nil
}
url = tempUrl
}
JSONEncoding과 URLEncoding을 나눈다. default는 formdata라고 생각하고, 그냥 URL를 만든다. JSONEncoding의 경우, JSONBody에 파라미터를 넣어서 전달하는 방식이기에, URL은 그냥 사용해되 무방하다. 하지만 path variable일 경우는 애초에 Enum에서 처리를 해서 나온다. URLEncoding의 경우, URLEncoding을 해야한다. 그렇기에 param을 url과 함께 넘겨 encoding을 한다. 이로 인해 request를 날릴 url이 정해진다.
var request = URLRequest(url: url)
request.httpMethod = apiModel.requestType.method
switch apiModel.contentType {
case .multiPartForm:
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
case .jsonData:
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
case .HTMLform:
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
case .noBody:
break
}
if let header = apiModel.header {
header.forEach { request.addValue($1, forHTTPHeaderField: $0)}
}
URLRequest 객체를 만들어준 뒤에, URLRequest 객체의 프로퍼티를 채워준다. 그리고 contentType에 따라서 header를 넣어주고, 그 외의 header가 있을 경우 역시 추가해준다.
HTTPbody를 만들때, contentType에 따라서 Httpbody가 다르게 만들어진다. JSONdata는 JSON형식으로 인코딩해서 Body를 만든다. multipartForm, HTMLForm 등 다양하다.
request.httpBody = createDataBody(parameter: apiModel.param, contentType: apiModel.contentType, url: apiModel.url)
private func createDataBody(parameter: [String: String?]?, contentType: ContentType, url: String) -> Data? {
var body = Data()
let lineBreak = "\r\n"
if let modelParameter = parameter {
switch contentType {
case .multiPartForm:
for (key, value) in modelParameter {
body.append(convertTextField(key: key, value: "\(value ?? "")"))
}
body.append("--\(boundary)--\(lineBreak)")
case .jsonData:
if let data = coder.encode(parameters: parameter){
body = data
}
case .HTMLform:
if let data = coder.encode(parameters: parameter, url: url){
body = data
}
case .noBody:
return nil
}
return body
} else {
return nil
}
}
URLSession으로 통신할때 마지막은 Task를 만든 다음, Task 객체를 resume()
시키는 것이다. 그리고 request함수의 return 형식이 URLSessionTaskProtocol? 이기에, 이에 맞춰서 return을 시킨다.
let task = session.makeDataTask(with: request) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let response = response as? HTTPURLResponse, (200...299).contains(response.statusCode) else {
completion(.failure(NetworkError.failResponse))
return
}
guard let data = data else {
completion(.failure(NetworkError.invalidData))
return
}
completion(.success(data))
}
task.resume()
return task
RxSwift를 도입하면서, 위의 request 함수를 Single을 return 하도록 변경했다.
func request(apiModel: APIable) -> Single<Data> {
return Single<Data>.create { (single) -> Disposable in
let request = self.request(apiModel: apiModel) { result in
switch result {
case .success(let data):
single(.success(data))
case .failure(let error):
single(.failure(error))
}
}
return Disposables.create()
}
}
https://github.com/Moya/Moya https://velog.io/@dlskawns96/iOS-Moya%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%9C-%EB%84%A4%ED%8A%B8%EC%9B%8C%ED%82%B9-Swift-Http-%ED%86%B5%EC%8B%A0