티스토리 뷰

이 글은 URLSession을 이용하여 서버와 통신하는 방법에 대해 다루겠습니다. 특정 Request 종류(GET, POST, DELETE 등)에 종속되지 않은 형태로 구현을 해 볼 예정입니다. 실제로 데이터를 보내기 위해 Enocde하는 과정 또는 데이터를 받아와서 Decode를 하는 과정이 배제되어 있을텐데 왜 배제 시켰는지는 작성하며 설명하도록 하겠습니다. 평상시 짜던 코드들을 정리하는 형식으로 작성하였으므로 오류가 있을 수 있습니다. 오류 발견시 댓글로 꼭 말씀해주세요! 시작해볼까요?

URLSession

URLSession 이 한 주제로도 방대한 양의 글을 쓸 수 있어서 깊게 들어가지는 않겠습니다. 간단히 소개를 해보면 URLSession과 관련 된 클래스들은 특정 URL에서 데이터를 다운받거나, 업로드를 할 수 있는 API를 제공해줍니다. URLSession의 instance를 통해 request를 요청을 할 때 아마 대부분 shared session 을 사용하셨을 것 같습니다. (저도 아직 custom session을 만들어 본 적이 없네요!) custom session을 쓰면 연결과 관련 있는 행동(cellular network를 써도 되는지, 하나의 호스트에 동시에 최대 몇 개까지의 connection을 만들 수 있는지 등)들을 커스텀할 수 있습니다.

오늘 작성할 Data Task 종류는 가장 기본적인 dataTask 에 대해서만 코드를 작성하겠지만 다양한 종류의 data task를 만들 수 있습니다. 그 종류와 특징들을 간단히 알아보자면 다음과 같겠습니다.

  • dataTask : NSData 객체들을 이용해 특정 URL로부터 정보를 주고 받기 위해 사용
  • uploadTask : 파일 형태의 데이터들을 보내기 위해 사용. background에서도 작동
  • downloadTask : 파일 형태로 데이터를 받아오기 위해 사용. background에서도 작동
  • webSocketTask : RFC6455(web socket protocol)에 정의된 WebSocket을 이용해서 TCP와 TLS를 통해 메세지 교환

무엇보다 가장 중요한 것은 비동기적으로 동작한다는 사실.

NetworkLayer를 만드는 이유

역할의 분리, 단일 책임 원칙

출처 : NAVER D2 이욱정

출처 : NAVER D2 이욱정 (YouTube)

지금 Clean Architecture에 대해 얘기하고자 하는 것은 아니지만 보시는 것 처럼 하나의 디자인 패턴(위의 경우 VIP)에서 데이터 흐름을 나타냈을 때 Entity, 즉 데이터 모델이 바뀌는 경우는 3가지가 있습니다. 1) 뷰에서 이벤트가 발생하여 데이터가 바뀌는 경우 2) 새로운 데이터를 Network API를 통해 받아오는 경우 3) 저장되어 있던 데이터를 Persistence API를 통해 받아오는 경우입니다.

1번의 경우처럼 View는 철저히 사용자의 이벤트가 발생했음을 알리고, 어떠한 공정과정을 통해(적용하는 디자인 패턴마다 달라서 퉁쳐버리기ㅋㅋ) 자신이 보여줘야하는 모델의 데이터가 바뀌면 업데이트를 하는 역할을 합니다. 따라서 그 역할의 범위를 벗어나는 네트워킹을 하는 것은 단일 책임 원칙에 위배되는 것입니다.

Custom Error Type 만들기

통신을 하다가 별의 별 이유로 에러가 나기 쉽기 때문에 어떤 종류의 에러가 발생했는지 로그를 남기기 위해 custom error type을 만듭니다. 물론 그냥 날라오는 error.localizedDescription 찍어도 되겠지만 우리 입맛대로 더 세분화해서 에러 처리를 할 수 있으니 더 좋은 것이 아닐까요? Error의 종류는 많습니다. http status code 구간대별로 그 case들을 나누는 것도 방법이겠지만 이 정도로만 해두고 넘어가겠습니다!


enum NetworkError: Error {
    case invalidURL
    case unsuccessfulResponse
    case APIInvalidResponse
    case unknownError(message: String)
    
    var localizedDescription: String {
        switch self {
        case .invalidURL: return "Invalid URL"
        case .unsuccessfulResponse: return "Unsuccessful Response"
        case .APIInvalidResponse: return "API Invalid Response"
        case .unknownError(let message): return "Unkown error : \(message)"
        }
    }
}

비동기 작업의 완료 - Callback (Notification, Delegation)

위에서도 언급했지만 URLSession은 비동기 작업을 수행합니다. 그렇기 때문에 필요한 데이터를 모두 받아왔을 때 뷰를 업데이트 하고 싶다면 다 받아온 시점을 알아야 하겠죠? 객체 간 소통하는 3가지 방법(Callback, Notification, Delegation) 중 Callback을 사용하도록 하겠습니다.

코드

import Foundation

enum NetworkError: Error {
    case invalidURL
    case unsuccessfulResponse
    case APIInvalidResponse
    case unknownError(message: String)
    
    var localizedDescription: String {
        switch self {
        case .invalidURL:
            return "Invalid URL"
        case .unsuccessfulResponse:
            return "Unsuccessful Response"
        case .APIInvalidResponse:
            return "API Invalid Response"
        case .unknownError(let message):
            return "Unkown error : \(message)"
        }
    }
}

enum RequestType: CustomStringConvertible {
    case get
    case post
    case delete
    
    var description: String {
        switch self {
        case .get:
            return "GET"
        case .post:
            return "POST"
        case .delete:
            return "DELETE"
        }
    }
}

typealias NetworkResult = ((Result<Data, NetworkError>) -> Void)

protocol NetworkServiceType {
    func request(request type: RequestType, url: String, body: Data?, completion: @escaping NetworkResult)
}

final class NetworkService: NetworkServiceType {
    
    private let session: URLSession
    
    init(session: URLSession = .shared) {
        self.session = session // custom session을 만들어 넣을 수도
    }
    
    func request(request type: RequestType = .get, url: String, body: Data?, completion: @escaping NetworkResult) {
        guard let url = URL(string: url) else {
            return completion(.failure(.invalidURL))
        }
        var urlRequest = URLRequest(url: url)
        urlRequest.httpMethod = type.description
        urlRequest.httpBody = body
        urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        session.dataTask(with: urlRequest) { data, response, error in
            guard error == nil else {
                completion(.failure(.unknownError(message: error?.localizedDescription ?? "Unkown")))
                return
            }
            guard let response = response as? HTTPURLResponse,
                  (200..<300).contains(response.statusCode) else {
                completion(.failure(.unsuccessfulResponse))
                return
            }
            guard let data = data else {
                completion(.failure(.APIInvalidResponse))
                return
            }
            completion(.success(data))
        }.resume()
    }
}

Encode & Decode 가 없는 이유

이 역시 단일책임의 원칙 때문이고 확장성을 고려한 사항이라고 볼 수 있습니다. 예를 들어 JSONDecoder를 사용해서 decode를 진행한다고 할 때 decoding 형식에 맞는 구체 타입을 제공해주어야 합니다.

private func fetchFromServer(url: String, completion: @escaping UseCaseResult) {
   network.request(request: .get, url: url, body: nil) { [weak self] result in
	    switch result {
         case .success(let data):
            guard let response = try? self?.deocder.deocde(Response.self], from: data) else {
               completion(.failure(.decodeError))
               return
            }
         case .failure(_):
            completion(.failure(.networkError))
      }
   }
}

그러나 API request는 다양한 곳에서 쓰일 수 있고 encode, decode 하는 과정에서 데이터의 형태 가공이 일어나기 때문에 중간 단계를 하나 더 두어 작업하는 것이 맞다고 판단하여 분리하였습니다.

 

'iOS' 카테고리의 다른 글

[iOS] AppDelegate & SceneDelegate  (2) 2021.01.17
[iOS] SceneDelegate & AppDelegate의 역할  (0) 2021.01.16
[Swift] defer 구문  (0) 2021.01.16
[Swift] Optional  (0) 2021.01.12
[Swift] Optional  (0) 2021.01.12
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
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
글 보관함