티스토리 뷰

Moya를 사용하였습니다.

이유는 URLProtocol을 사용하지 않고 응답으로 Mock데이터를 받고 싶었습니다.

목적에 맞게 SampleData를 사용하여 MockData를 응답으로 받는 serviceStub를 구현하여 테스트하였습니다.

또 하나의 장점은 BaseAPI를 제공하였기에, URLRequest를 매우 편한 방법으로 사용하고, 재활용할 수 있었습니다.

 

얼마전 Alamofire내부 코드를 읽어보았습니다.

- 올해 초 부트캠프를 하며 다른 조원의 PR을 보고 의구심을 갖게 되었습니다.

PR내용 중 일부.

'가장 네트워크쪽에 있을 Alamofire를 요청하는 부분이 View를 바로 설정할 수 있도록 응답이 main에서 실행된다..? 그럴리가 없는데..'

분명 이유가 있을것이다!!

내부의 코드를 읽고 주석을 읽어보니 경쟁상태를 방지하기 위해 내부적으로 SerialQueue로 응답클로져를 감싸고 있었습니다.

URLSession보다 요청을 편하게 할 수 있다는 장점만으로 사용하는 경우도 있는 것 같던데, 이런 내부적인 것들 까지 생각해주다니..

Alamofire 섬세함에 감동했습니다..

 

'이거 되게 재밌어보인다~'

라는 생각으로 네트워크라이브러리를 직접 만들어보게 되었습니다.

 

- 네트워크 라이브러리 제작기.

1. 작명하기.

2. 기능 정의하기.

3. 배포하기.

 

1.

이름을 뭘로 짓지?

MyNewNetwork? WhatUsuallyUseNetworkFeature? BestOfNetworkFeature?

얼마전 넷플릭스에서 시리즈 영화를 보았는데.. 너무 재밌어서 무호흡으로 4편 모두 봐버린 영화!

바로 Bourne 시리즈..!

Bourne.Request() 이거다..!

엥 근데 이미 있네 ㅜㅜ 그럼 JasonBourne으로!

 

2.

기능 정의하기.

Moya에서 편했던 기능 + Alamofire에서 편했던 기능 가져오기!

Moya에서의 Mock데이터 응답기능과 Request를 쉽게 생성가능한 BaseTarget!

+

Alamofire에서 응답 클로져를 SerialQueue로 감싸 경쟁상태를 예방해주는 방식

일단은 요렇게!

( 추후 추가한다면 요즘 핫한 Async Await + 디스크캐싱을 위한 DownloadTask까지! )

 

그리고 모든 기능은 URLSession기반으로 요청하게 해야겠다! 

 

3.

배포하기.

실수를 했는지.. 0.1.0에 아무런 기능이 안들어가있다!?

아..안돼...

그래서 태그를 여러개 바꿔가며 0.1.1, 0.1.2를 생성하였지만

pod lint spec 시 계속 ReplaceMe.swift파일이 다른 클래스파일들을 찾을 수 없다는 오류 발생 ㅜㅜ

 

알고보니 태그를 다르게 하더라도 기존에 생성되었던 태그는 더 이상 코드 업데이트가 안되는 듯 하였다..

(git push origin 0.1.2를 하더라도 everything-up-to-date라 하였다..)

 

그래서 새로운 태그로 pod lint spec을 하였더니.. 

[iOS] file patterns: The `source_files` pattern did not match any file

에러가 발생했다.. 으으으 진짜 왜그러는겨~

이렇게도 바꿔보고 저렇게도 바꿔봤지만..

결국 태그를 0.2.0으로 업데이트 하였더니 성공했다...

 

- JasonBourne 네트워크 라이브러리 제작일기

 

------

구현

public protocol APITarget {
    /// The target's base `URL`.
    var baseURL: URL { get }

    /// The path to be appended to `baseURL` to form the full `URL`.
    var path: String { get }

    /// The HTTP method used in the request.
    var method: APIMethod { get }

    /// The type of HTTP task to be performed.
    var task: Parameter { get }

    /// The headers to be used in the request.
    var headers: [String: String]? { get }
    
    /// The MockData response when Bourne is StubMode
    var mockData: Data { get }
}

public enum APIMethod: String {
    case get = "GET"
    case post = "POST"
}

public enum Parameter {
    case requestParameters(parameters: [String: String])
    case requestBody(data: Data)
}

public enum ResponseSpeed {
    case notStub
    case immediately
    case normal
    case almostNever
}

Moya의 BaseTarget과 거의 같습니다.

ResponseSpeed는 Mock데이터로 설정 시 응답이 오는 속도를 설정하기 위한 enum입니다.

 

public class Bourne {
    private let session = URLSession.shared
    private let stubMode: ResponseSpeed
    
    public init() {
        stubMode = .notStub
    }
    
    public init(stubMode: ResponseSpeed) {
        self.stubMode = stubMode
    }
    
    public func request(api: APITarget, completion: @escaping ((Result<Data, BournError>) -> Void)) {
        switch stubMode {
        case .notStub:
            requestAPI(api: api, completion: completion)
        case .immediately:
            return completion(.success(api.mockData))
        case .normal:
            DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
                return completion(.success(api.mockData))
            }
        case .almostNever:
            DispatchQueue.main.asyncAfter(deadline: .now() + 1000) {
                return completion(.success(api.mockData))
            }
        }
    }
    
    private func requestAPI(api: APITarget, completion: @escaping ((Result<Data, BournError>) -> Void)) {
        let serialQueue = DispatchQueue(label: "serialQueue")
        let handler: ((Data?, URLResponse?, Error?) -> Void) = { data, response, error in
            guard error == nil else {
                return completion(.failure(.networkError))
            }
            
            guard let httpURLResponse = response as? HTTPURLResponse else {
                return completion(.failure(.httpResponse))
            }
            
            guard (200...299) ~= httpURLResponse.statusCode else {
                return completion(.failure(.responseCode))
            }
            
            guard let data = data else {
                return completion(.failure(.nilDataInSession))
            }
            
            serialQueue.async {
                completion(.success(data))
            }
        }
        
        switch api.method {
        case .get:
            guard let url = makeURL(from: api) else {
                return completion(.failure(.urlInvailed)) }
            session.dataTask(with: url, completionHandler: handler)
                .resume()
        case .post:
            guard let urlRequest = makeURLRequest(from: api) else {
                return completion(.failure(.urlInvailed)) }
            session.dataTask(with: urlRequest, completionHandler: handler)
                .resume()
        }
    }
    
    private func makeURL(from api: APITarget) -> URL? {
        var url = api.baseURL
        url.appendPathComponent(api.path)
        guard var components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { return nil }
        switch api.task {
        case .requestParameters(let parameter):
            components.queryItems = parameter.map {
                URLQueryItem(name: $0, value: $1)
            }
        case .requestBody(_):
            break
        }
        return components.url
    }
    
    private func makeURLRequest(from api: APITarget) -> URLRequest? {
        var url = api.baseURL
        url.appendPathComponent(api.path)
        var urlRequest = URLRequest(url: url)
        switch api.task {
        case .requestBody(let data):
            urlRequest.httpBody = data
        case .requestParameters(_) :
            break
        }
        urlRequest.httpMethod = api.method.rawValue
        let _ = api.headers?.compactMap { $0 }
            .map { urlRequest.addValue($0.key, forHTTPHeaderField: $0.value) }
        return urlRequest
    }
}

핵심코드입니다.

생성자 인자값으로 stub객체로 쓸지에 대한 유무를 설정할 수 있습니다.

실제 요청코드는 private을 사용하여 모두 숨겨두었습니다.

 

응답 클로져를 SerialQueue로 감싸 사용자가 사용하는 응답코드가 해당 큐에서 실행 됐는 지 디버깅을 통해 확인하였습니다.

 

사실 뭔가 엄청난 로직(?)이 있는 건 아니지만 이렇게 하나씩 만들어내면 공부한

내용들 정리도 하는 것 같고 보람도 느끼게 되는 것 같습니다. ㅎㅎ

감사합니다.

https://github.com/rising-jun/JasonBourne

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
글 보관함