iOS - RxSwift에 대응하는 네트워크 세션 만들기

Wonbi·2024년 3월 31일
0

iOS 공부

목록 보기
3/4
post-thumbnail

들어가기 전에..

AlamofireMoya를 두고 굳이..? 라고 생각이 들 수도 있지만 직접 네트워크 객체를 만들어보고 싶기도 했고, 해당 라이브러리의 기능을 거의 사용하지 않고 일부 기능만 사용할 예정이기에, 필요한 부분만 만들어 쓰는게 더 간단할 것이라 생각했다.

그럼 만들어 보자

MoyaTargetType 프로토콜을 통한 리퀘스트 지정 방식은 정말 좋은 방법이라 생각해 필요한 부분만 뽑아서 만들어보고자 했다.

BaseURLType

enum BaseURLType {
    case api
    
    var urlString: String {
        switch self {
        case .api: return "서버 URL 주소"
        }
    }
}
  • BaseURL은 enum으로 정의해 각각의 케이스별 URL주소를 가지도록 했다. 만약 테스트용 서버가 따로 존재하거나, 서버 주소가 여러개일 경우, 각 케이스별로 구분해서 만들어야 하니까.

HTTPMethod

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
    case patch = "PATCH"
    case delete = "DELETE"
}
  • 음.. 말하지 않아도 아는 그것.

Path

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"
        }
    }
}
  • Path는 EndpointProtocol을 채택해 관리하도록 했다. 로그인 관련 Path, 회원가입 관련 Path 등 따로따로 관리할 수 있을테니까.

Task

enum Task {
    case requestPlain
    case requestHttpBody(Data)
    case requestParameters(parameters: [String: Any])
    case requestHttpBodyAndParameters(body: Data, parameters: [String: Any])
}
  • 각각의 리퀘스트 종류에 따라 필요한 파라메터나 HttpBody를 추가할 수 있도록 했다.
  • 이렇게하면 어떠한 파라메터도 필요없이 단순히 데이터를 요청하는 리퀘스트는 requestPlain만 사용하면 파라메터가 없음을 명시하지 않아도 된다.

Error

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"
        }
    }
}
  • 리퀘스트를 빌더 패턴을 통해 만들기로 결정했다. 빌더 패턴에서 리퀘스트를 만들 때 특정 데이터가 들어오지 않았다면 에러를 내도록 했다.

RequestBuilder

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를 추가해 반환한다.

NetworkSession

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
    }
}
  • 이제 세션을 만들어서 데이터를 반환해보자. RxSwift에 대응하게 만들 것이고, 한번의 요청이 끝난 후에는 해당 스트림이 더이상 필요하지 않기 때문에 Single을 활용하기로 했다.
  • Single.create() 타입 메서드를 이용해 요청을 보내고 그 결과값에 따라 분기를 처리한다. 여기서 분기처리는 URLSession의 dataTask메서드를 사용한다.
  • 한가지 주의할 점은 Disposables을 리턴할 때 해당 테스크를 꼭 캔슬해 메모리에 테스크가 남지 않도록 처리해줘야 한다.

Result

  • AlamofireMoya는 잘 만든 라이브러리다. 만들고 보니 라이브러리를 쓰는것도 좋은 방법일 것이라 생각했다.
  • 그래도 만들면서 많이 공부할 수 있고 나만의 네트워크 세션 객체를 만든거 같아 뿌듯했다.

0개의 댓글