Taehyeon-Kim / Taehyeon-Kim.github.io

The life of recording is unbreakable
https://taehyeon-kim.github.io
MIT License
1 stars 0 forks source link

iOS101: Network Layer Modeling #19

Open Taehyeon-Kim opened 10 months ago

Taehyeon-Kim commented 10 months ago

Network Layer Modeling

네트워크 요청을 하는 것의 핵심은 Request, Response이다. 어떤 데이터를 담아서 요청서를 만들고, 올바르게 데이터가 넘어왔는지 뜯어서 확인하고 제대로 된 요청이 오지 않았으면 에러를 적절하게 사용자에게 보여줄 수 있어야 한다.

1. Request

Request를 할 때에는 다음과 같은 정보들이 필요하다:

  1. HTTPMethod
  2. URL
  3. Path
  4. QueryItems
  5. Header
  6. Body

위의 요소들을 잘 조합해서 Request를 만들면 된다. Request에 필요한 정보들은 위와 같이 제한적이기에 추상화를 시킬 수 있다. iOS에서 추상화를 할 때에 일반적으로 Protocol을 많이 사용하므로 Protocol을 이용한 추상화를 해보도록 하겠다. 반드시 Protocol 추상화해야하는 것은 아니니 고정 관념을 가지고 코드르 바라보지 말자.

protocol BaseAPIRequest {
  associatedtype Response: Decodable

  var method: HTTPMethod { get }
  var baseURL: URL { get }
  var path: String { get }
  var queryItems: [URLQueryItem]? { get }
  var headers: [String: String]? { get }
  var body: Encodable? { get }
}

extension BaseAPIRequest {
  func buildURLRequest() throws -> URLRequest {
    // ... 위의 정보로 URLRequest 생성
  }
}

특정 URI에 대한 Request는 위의 프로토콜을 준수하도록 만들면 된다. 각 Request마다 필요한 구체적인 정보가 다르기 때문이다.

struct WeatherRequest: BaseAPIRequest {
  typealias Response = WeatherResponse

  /// ... 구체적인 정보 정의
}

2. Response

Response는 네트워크 요청을 통해서 받아온 Data를 매핑할 객체이다. Decodable 프로토콜을 준수해야 Data를 앱에서 사용할 수 있는 객체로 매핑할 수 있다.

import Foundation

struct WeatherResponse: Identifiable, Decodable {

  struct Location: Decodable {
    let name: String
    let country: String
  }

  struct Condition: Decodable {
    let text: String
    let icon: String

    var iconString: String {
      "https:" + icon
    }
  }

  struct Current: Decodable {
    let tempC: Double
    let condition: Condition

    var tempCString: String {
      String(tempC)
    }

    enum CodingKeys: String, CodingKey {
      case tempC = "temp_c"
      case condition
    }
  }

  var id: UUID {
    UUID()
  }
  let location: Location
  let current: Current
}

3. Client

HTTPClient를 만들어주어야 한다. 이 객체의 핵심 역할은 Request를 send(perform, request)하는 것이다. 외부 서버에 자원을 달라고 요청을 수행하는 역할을 해야 한다. SRP를 지키기 위해서는 Decoding, Data Parsing, Error Handling 등의 처리는 다른 객체가 담당하도록 만드는 것이 좋지만 어느 정도 범주 내에서는 Client 내에서 처리해줘도 괜찮다고 생각한다. 객체를 너무 나누게 되면 그 과정에서 들어가는 리소스도 상당하기 때문이다. 그렇기에 트레이드-오프를 잘 고려해야 할 것 같다.

struct WeatherClient {
  private let session: URLSession = {
      let config = URLSessionConfiguration.default
      return URLSession(configuration: config)
  }()

  private let decoder: JSONDecoder = {
      let decoder = JSONDecoder()
      decoder.keyDecodingStrategy = .convertFromSnakeCase
      return decoder
  }()

  public init() {}

  func perform<Request: APIRequest>(request: Request) async throws -> Request.Response {
    let result =  try await session.data(for: request.buildURLRequest())
    try validate(data: result.0, response: result.1)
    return try decoder.decode(Request.Response.self, from: result.0)
  }

  func validate(data: Data, response: URLResponse) throws {
    guard let code = (response as? HTTPURLResponse)?.statusCode else {
      throw APIError.connectionError
    }

    guard (200..<300).contains(code) else {
      throw APIError.apiError
    }
}

4. Result

Swift Concurreny, Combine Framework 등의 사용으로 좀 더 간결하고 직관적인 네트워크 레이어 구축이 가능해졌다. 다음 작업은 프로젝트 초기에 잘 구축해놓으면 실제 구현하는 부분 외에서는 코드 변경이 많이 일어나지 않기 때문에 잘 고민해두는 것이 좋다.

위에서 살펴본 내용은 가장 기본적인 내용이고 사실 이외에 고려할 것들은 정말 많다. Network Connection, Timeout, Retry, Authentication Flow, Error handling 등등은 다시 정리해보도록 하자.