URLSession, Alamofire, Moya 간의 선택
지금껏 iOS 앱 개발을 하면서 서버통신 시에 Moya Library를 주로 사용해 왔습니다. Moya는 Alamofire 라이브러리를 한층 더 Wrapping하여 사용하기 편리하도록 한 라이브러리입니다. Wrapping은 사용의 편의성이라는 장점도 제공하지만 URLSession에 접근하기 어렵다는 한계 또한 가지고 있습니다. 최근 이를 느끼고 한 계층 낮은 Alamofire 라이브러리를 사용할지, 아니면 순수 URLSession을 Module화하여 사용할지 고민하다가, 그래도 조금은 Module화가 되어있는 Alamofire 라이브러리를 사용하는 것이 돌발상황 대처에 좋을 것 같아 Alamofire를 사용하기로 결정했습니다.
개인적으로 Moya의 사용에 익숙해져 있고, 체계적으로 기능이 분리되어 있는 라이브러리의 특성상 규모가 커질수록 유지보수에 장점이 있다는 점을 느꼈기에 Alamofire를 Moya처럼 만들어보기로 했습니다.
아래는 그 과정에서 참고한 자료입니다.
https://ios-development.tistory.com/797
Networking Layer 구조
우선 전체적인 구조는 아래와 같습니다.
* Foundation : Module화의 기틀을 제공하는 클래스, 구조체들이 정의되어 있습니다.
- Router : URLRequestConvertible을 채택한 프로토콜로, Routers 폴더에 있는 구체 타입들이 이 프로토콜을 채택합니다.
- BaseService : 서버통신을 요청하기 위한 Service들이 공통적으로 상속하는 superClass입니다. 커스텀 AFManger와 judgeStatus 메서드가 존재합니다.
- HeaderType : Router에서 반환할 헤더의 종류들이 정의되어 있습니다.
- NetworkResult : 서버통신의 결과를 저장할 열거형 타입입니다.
* NetworkConstants : 서버통신에 필요한 BaseURL 또는 timeOut과 같은 상수들을 관리합니다.
* Routers : Router 프로토콜을 채택하여 header, path, parameter 등을 커스텀하여 request를 만들게 해줍니다.
* Service : 실제로 서버통신을 요청하기 위해 사용해야 하는 클래스입니다. 여러 endpoint들을 정의해두고 사용할 수 있습니다.
Foundation
우선 Module화의 기틀을 제공하는 클래스, 구조체들이 정의된 Foundation 폴더부터 살펴보겠습니다.
* Router
Router는 URLRequestConvertible 프로토콜을 상속하는 프로토콜입니다. 모듈화에 있어 가장 기본이 되는 아이디어가 URLRequestConvertible 프로토콜입니다.
URLRequestConvertible를 채택하고, asURLRequest()와 아래 메서드를 구현해주면 원하는 방식으로 request를 가공해줄 수 있습니다. 따라서, asURLRequest() 메서드에서 request를 만드는 과정에 접근할 수 있도록 프로토콜에 method, path, parameter, headerType과 같은 프로퍼티들을 추가로 선언한다면! endPoint별로 case를 나눠서 적절한 request를 만들 수 있지 않을까? 라는 생각을 하게 된 것입니다.
우선 전체적인 코드는 아래와 같습니다. ( 추후 3, 4편에서 전체적인 코드를 수정하니 참고해주세요 )
import Foundation
import Alamofire
protocol Router: URLRequestConvertible {
var baseURL: String { get }
var method: HTTPMethod { get }
var path: String { get }
var parameters: RequestParams { get }
var header: HeaderType { get }
}
// MARK: asURLRequest()
extension Router {
// URLRequestConvertible 구현
func asURLRequest() throws -> URLRequest {
let url = try baseURL.asURL()
var urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method)
urlRequest = self.makeHeaderForRequest(to: urlRequest)
return try self.makePrameterForRequest(to: urlRequest, with: url)
}
private func makeHeaderForRequest(to request: URLRequest) -> URLRequest {
...
}
private func makePrameterForRequest(to request: URLRequest, with url: URL) throws -> URLRequest {
...
}
}
// MARK: baseURL & header
extension Router {
var baseURL: String {
return URLConstants.baseURL
}
var header: HeaderType {
return HeaderType.default
}
}
// MARK: ParameterType
enum RequestParams {
case query(_ parameter: Codable?)
case body(_ parameter: Codable?)
case requestParameters(_ parameter: [String : Any])
}
// MARK: toDictionary
extension Encodable {
func toDictionary() -> [String: Any] {
guard let data = try? JSONEncoder().encode(self),
let jsonData = try? JSONSerialization.jsonObject(with: data),
let dictionaryData = jsonData as? [String: Any] else { return [:] }
return dictionaryData
}
}
먼저 아래 asURLRequest를 보면
func asURLRequest() throws -> URLRequest {
let url = try baseURL.asURL()
var urlRequest = try URLRequest(url: url.appendingPathComponent(path), method: method)
urlRequest = self.makeHeaderForRequest(to: urlRequest)
return try self.makePrameterForRequest(to: urlRequest, with: url)
}
1. urlRequest를 만들어주고
2. makeHeaderForRequest(to: urlRequest)
3. makeParameterForRequest(to: urlRequest, with: url)
4. 가공된 request를 return합니다.
2번과 3번은 case별로 request에 헤더와 파라미터를 부여합니다.
makeHeaderForRequest에서는, hearType별로 header를 request에 부여해주고 있습니다.
private func makeHeaderForRequest(to request: URLRequest) -> URLRequest {
var request = request
switch header {
case .default:
request.setValue(HeaderContent.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
case .withToken:
request.setValue(HeaderContent.json.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
request.setValue(HeaderContent.tokenSerial.rawValue, forHTTPHeaderField: HTTPHeaderField.accesstoken.rawValue)
case .multiPart:
request.setValue(HeaderContent.multiPart.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
case .multiPartWithToken:
request.setValue(HeaderContent.multiPart.rawValue, forHTTPHeaderField: HTTPHeaderField.contentType.rawValue)
request.setValue(HeaderContent.tokenSerial.rawValue, forHTTPHeaderField: HTTPHeaderField.accesstoken.rawValue)
}
return request
}
makeParameterForRequest에서는, RequestParams별로 request에 parameter를 부여해주고 있습니다.
private func makeParameterForRequest(to request: URLRequest, with url: URL) throws -> URLRequest {
var request = request
switch parameters {
case .query(let query):
let params = query?.toDictionary() ?? [:]
let queryParams = params.map { URLQueryItem(name: $0.key, value: "\($0.value)") }
var components = URLComponents(string: url.appendingPathComponent(path).absoluteString)
components?.queryItems = queryParams
request.url = components?.url
case .body(let body):
let params = body?.toDictionary() ?? [:]
request.httpBody = try JSONSerialization.data(withJSONObject: params, options: [])
case .requestParameters(let requestParams):
let params = requestParams
request.httpBody = try JSONSerialization.data(withJSONObject: params, options: [])
}
return request
}
또한 아래와 같이 baseURL과 header를 초기구현하여 편의성을 높였습니다.
extension Router {
var baseURL: String {
return URLConstants.baseURL
}
var header: HeaderType {
return HeaderType.default
}
}
지금까지는 alamofire에 내장된 URLRequestConvertible을 상속한 Router 프로토콜을 만들고, request를 만드는 흐름을 제어했습니다. 다음으로는 이렇게 만든 Router를 채택하는 주체이자, 모든 Service들의 superClass인 BaseService에 대해서 알아보겠습니다.
* BaseService
서버통신을 요청하기 위한 Service들이 공통적으로 상속하는 SuperClass이며, 커스텀 AFManger와 judgeStatus 메서드가 존재합니다. 한번 살펴보겠습니다.
전체 코드는 아래와 같습니다.
import Foundation
import Alamofire
class BaseService {
@frozen enum DecodingMode {
case model
case message
case general
}
let AFManager: Session = {
var session = AF
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = NetworkEnvironment.requestTimeOut
configuration.timeoutIntervalForResource = NetworkEnvironment.resourceTimeOut
session = Session(configuration: configuration)
return session
}()
func judgeStatus<T: Codable>(by statusCode: Int, _ data: Data, type: T.Type, decodingMode: DecodingMode = .general) -> NetworkResult<Any> {
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(GeneralResponse<T>.self, from: data)
else { return .pathErr }
switch statusCode {
case 200..<300:
switch decodingMode {
case .model:
return .success(decodedData.data ?? "None-Data")
case .message:
return .success(decodedData.message ?? "None-Data")
case .general:
return .success(decodedData)
}
case 400..<500:
return .requestErr(decodedData.status)
case 500:
return .serverErr
default:
return .networkFail
}
}
}
우선 DecodingMode 열거형부터 살펴보겠습니다. model, message, general 타입은 서버통신이 성공하고, decoding 시에 어떤 데이터를 어떤 형태로 return할지에 대한 선택지입니다. success에서 GeneralResponse 모델 전체가 필요할 수 있고, model만 필요할 수 있으며 message만 필요할 수 있기 때문에 이러한 선택지를 미리 구성해 놓았습니다.
@frozen enum DecodingMode {
case model
case message
case general
}
다른 요소에 대해 알아보기에 앞서 GeneralResponse에 대해서도 알아보겠습니다. General Response란 이름 그대로와 같이 서버에서 오는 일반적인 응답의 모델입니다. 통상적으로 success/failure, status, message, data의 형식으로 응답이 오기 때문에 이런 식으로 정의되어 있습니다. 보통의 경우 presentation에서는 data에 관심이 있기 때문에 이를 따로 가공하는 과정이 필요하고, 그 decoding 과정을 간편하게 하기 위해 DecodingMode 열거형을 사용했습니다.
import Foundation
struct GeneralResponse<T> {
let success : Bool
let status: Int
let message: String?
let data: T?
enum CodingKeys: String, CodingKey {
case success
case status
case message
case data
}
}
extension GeneralResponse: Decodable where T: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
success = try container.decode(Bool.self, forKey: .success)
status = try container.decode(Int.self, forKey: .status)
message = try? container.decode(String.self, forKey: .message)
data = try? container.decode(T.self, forKey: .data)
}
}
다음으로 AFMagner입니다. AFManager는 URLSession에 존재하는 timeout 관련 상수들을 정해주기 위해 선언했으며, Service 전역적으로 적용되어야 할 요소가 있을때 사용합니다.
let AFManager: Session = {
var session = AF
let configuration = URLSessionConfiguration.af.default
configuration.timeoutIntervalForRequest = NetworkEnvironment.requestTimeOut
configuration.timeoutIntervalForResource = NetworkEnvironment.resourceTimeOut
session = Session(configuration: configuration)
return session
}()
마지막으로 judgeStatus<T: Codable> 메서드입니다. 이 메서드는 각 Service에 존재하는 request 메서드들이 데이터를 어떤 형식으로 받을지에 대해 선택하고, statusCode와 DecodingMode에 따라 다른 결과값을 return합니다. decodingMode의 기본 타입은 .general로 되어 있습니다. 따라서 GeneralResponse 데이터 전체가 넘어가게 됩니다.
func judgeStatus<T: Codable>(by statusCode: Int, _ data: Data, type: T.Type, decodingMode: DecodingMode = .general) -> NetworkResult<Any> {
let decoder = JSONDecoder()
guard let decodedData = try? decoder.decode(GeneralResponse<T>.self, from: data)
else { return .pathErr }
switch statusCode {
case 200..<300:
switch decodingMode {
case .model:
return .success(decodedData.data ?? "None-Data")
case .message:
return .success(decodedData.message ?? "None-Data")
case .general:
return .success(decodedData)
}
case 400..<500:
return .requestErr(decodedData.status)
case 500:
return .serverErr
default:
return .networkFail
}
}
지금까지 Alamofire를 Moya처럼 사용하기 위해 case별로 request를 만들 수 있도록 하는 기초 요소들이 포함된 Foundation을 살펴봤습니다. 다음 편에서는 이렇게 구현한 Router와 BaseService를 채택하여 endpoint별 request를 만드는 Routers와 Services에 대해 알아보겠습니다!
'iOS' 카테고리의 다른 글
[Swift] Coordinator Pattern 기본!! With RayWanderlich Tutorial! (0) | 2022.05.19 |
---|---|
[Swift] Alamofire를 Moya처럼 사용해보자! By Router Pattern (2편 - Services, Routers 구현) (4) | 2022.05.19 |
[RxSwift] RxSwift 기본 (1편 - Observable, Observer, Subject) (1) | 2022.04.16 |
[Swift 문법] Optional type Closure와 @escaping (2) | 2022.03.20 |
[MVVM] Closure를 이용한 MVVM 구현(With SearchController, Throttle) (0) | 2022.03.20 |