Alamofire
랑 Moya
를 두고 굳이..? 라고 생각이 들 수도 있지만 직접 네트워크 객체를 만들어보고 싶기도 했고, 해당 라이브러리의 기능을 거의 사용하지 않고 일부 기능만 사용할 예정이기에, 필요한 부분만 만들어 쓰는게 더 간단할 것이라 생각했다.
Moya
의 TargetType
프로토콜을 통한 리퀘스트 지정 방식은 정말 좋은 방법이라 생각해 필요한 부분만 뽑아서 만들어보고자 했다.
enum BaseURLType {
case api
var urlString: String {
switch self {
case .api: return "서버 URL 주소"
}
}
}
enum
으로 정의해 각각의 케이스별 URL주소를 가지도록 했다. 만약 테스트용 서버가 따로 존재하거나, 서버 주소가 여러개일 경우, 각 케이스별로 구분해서 만들어야 하니까.enum HTTPMethod: String {
case get = "GET"
case post = "POST"
case patch = "PATCH"
case delete = "DELETE"
}
protocol EndpointProtocol {
var path: String { get }
}
enum LoginEndpotint: EndpointProtocol {
case login, unregister
var path: String {
switch self {
case .login: return "/login"
case .unregister: return "/unregister"
}
}
}
EndpointProtocol
을 채택해 관리하도록 했다. 로그인 관련 Path, 회원가입 관련 Path 등 따로따로 관리할 수 있을테니까.enum Task {
case requestPlain
case requestHttpBody(Data)
case requestParameters(parameters: [String: Any])
case requestHttpBodyAndParameters(body: Data, parameters: [String: Any])
}
requestPlain
만 사용하면 파라메터가 없음을 명시하지 않아도 된다.enum URLRequestBuildError: Error, CustomStringConvertible {
var description: String { self.errorDescription }
case emptyBaseURL
case emptyEndpoint
case emptyMethod
case emptyTask
case urlComponentError
var errorDescription: String {
switch self {
case .emptyBaseURL: return "BASE_URL_IS_REQUIRED"
case .emptyEndpoint: return "ENDPOINT_IS_REQUIRED"
case .emptyMethod: return "HTTP_METHOD_IS_REQUIRED"
case .emptyTask: return "TASK_IS_REQUIRED"
case .urlComponentError: return "URL_COMPONENT_ERROR"
}
}
}
final class RequestBuilder {
private var baseURL: BaseURLType?
private var endpoint: EndpointProtocol?
private var method: HTTPMethod?
private var headers: [String: String]?
private var task: Task?
func withBaseURL(_ url: BaseURLType) -> RequestBuilder {
self.baseURL = url
return self
}
func withHTTPMethod(_ method: HTTPMethod) -> RequestBuilder {
self.method = method
return self
}
func withEndpoint(_ endpoint: EndpointProtocol) -> RequestBuilder {
self.endpoint = endpoint
return self
}
func withHeader(_ header: [String: String]) -> RequestBuilder {
self.headers = header
return self
}
func withTask(_ task: Task) -> RequestBuilder {
self.task = task
return self
}
func buildURL() -> Result<URLRequest, URLRequestBuildError> {
defer {
self.baseURL = nil
self.endpoint = nil
self.method = nil
self.headers = nil
self.task = nil
}
guard let baseURL else { return .failure(.emptyBaseURL) }
guard let endpoint else { return .failure(.emptyEndpoint) }
guard let method else { return .failure(.emptyMethod) }
guard let task else { return .failure(.emptyTask) }
var urlComponent = URLComponents(string: baseURL.urlString + endpoint.path)
if case let .requestParameters(parameters) = task {
urlComponent?.queryItems = parameters.map { URLQueryItem(name: $0, value: "\($1)") }
}
if case let .requestHttpBodyAndParameters(_, parameters: parameters) = task {
urlComponent?.queryItems = parameters.map { URLQueryItem(name: $0, value: "\($1)") }
}
guard let url = urlComponent?.url else { return .failure(.urlComponentError) }
var request = URLRequest(url: url)
if case let .requestHttpBody(httpBody) = task {
request.httpBody = httpBody
}
if case let .requestHttpBodyAndParameters(body: httpBody, _) = task {
request.httpBody = httpBody
}
if let headers {
headers.forEach{ request.setValue($1, forHTTPHeaderField: $0) }
}
request.httpMethod = method.rawValue
return .success(request)
}
}
URLRequest
를 만드는 과정이다. buildURL
메서드에서 입력받은 데이터를 가지고 URLComponents
를 만들고, Task
에 따라 쿼리를 추가한다.URL
을 만들고 URLRequest
를 만들어 Body
, Header
, Method
를 추가해 반환한다.final class NetworkSession: NetworkSessionProtocol {
func dataTask(_ request: URLRequest) -> Single<Data> {
return Single<Data>.create { emitter in
let task = URLSession.shared.dataTask(with: request) { data, reponse, error in
guard let httpResponse = reponse as? HTTPURLResponse else {
emitter(.failure(NetworkSessionError.unknownError))
return
}
guard 200...299 ~= httpResponse.statusCode, error == nil else {
emitter(.failure(self.configureHTTPError(errorCode: httpResponse.statusCode)))
return
}
guard let data = data else {
emitter(.failure(NetworkSessionError.emptyDataError))
return
}
emitter(.success(data))
}
task.resume()
return Disposables.create {
task.cancel()
}
}
}
private func configureHTTPError(errorCode: Int) -> Error {
return NetworkSessionError(rawValue: errorCode) ?? NetworkSessionError.unknownError
}
}
Single
을 활용하기로 했다.Single.create()
타입 메서드를 이용해 요청을 보내고 그 결과값에 따라 분기를 처리한다. 여기서 분기처리는 URLSession의 dataTask메서드를 사용한다. Disposables
을 리턴할 때 해당 테스크를 꼭 캔슬해 메모리에 테스크가 남지 않도록 처리해줘야 한다.Alamofire
랑 Moya
는 잘 만든 라이브러리다. 만들고 보니 라이브러리를 쓰는것도 좋은 방법일 것이라 생각했다.