Network Layer Modeling

taekki
daily-monster
Published in
5 min readFeb 2, 2024

안녕하세요 금요괴물 태끼입니다. 오늘은 네트워크 요청에 대한 이야기를 가볍게 해보고자 합니다.

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

1. Request

Request를 할 때에는 다음과 같은 정보들이 필요합니다:
1. HTTPMethod
2. URL
3. Path
4. QueryItems
5. Header
6. Body
7. …

위의 요소들을 잘 조합해서 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 = ProductResponse
/// … 구체적인 정보 정의
}

2. Response

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

import Foundation

struct ProductResponse: Identifiable, Decodable {
var id: UUID {
UUID()
}
let name: String
let price: Int
}

3. Client

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

struct ProductClient {
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() {}
}

extension ProductClient {
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...299).contains(code) else {
throw APIError.apiError
}
}
}

4. Result

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

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

--

--