URLSession을 사용해서 간단한 데이터를 받아오기 – Swift

HTTP통신을 통해서 서버로부터 이미지나 텍스트와 같은 작은 파일을 가져오거나, API 통신을 하는 방법을 살펴보자. NSURLSession은 NSURLConnection과는 달리 ‘간단한’ 작업을 위해서는 정말 간단한 코드로만 작업을 수행할 수 있게 해준다. 대신에 NSURLSession은 완전한 비동기 통신만을 지원하는데, 해당 API는 가급적 델리게이트의 작성량을 최대한 줄일 수 있도록 디자인되어 있으므로 이러한 작업의 경우, 코드도 매우 단순해진다.

기본적인 원리에 대해서 살짝 언급하자면 다음과 같다.

  • 통신을 위해서는 NSURLSession 객체가 필요하다. 세션 객체가 있다면 이로부터 NSURLSessionDataTask 객체를 생성할 수 있다.
  • 모든 작업은 비동기로 이루어지므로, 델리게이트가 필요할 것이나 데이터 작업을 생성할 때 완료 핸들러를 제공하는 것으로 대체할 수 있다.
  • 작업 객체를 생성했다면, resume() 메소드를 호출해서 통신을 개시할 수 있다.
  • 이 때 네트워크로 전송되는 데이터는 NSData 클래스로 감싸지게 된다.

세션 객체를 만드는 것도 사실 간단한데, 기본적인 API 통신의 경우에는 싱글톤인 공유 세션을 사용할 수 있다. 공유 세션은 URLSession의 shared 프로퍼티(Objective-C 의 경우, [NSURLSession sharedSession])를 통해서 접근할 수 있다. 만약 HTML 페이지의 소스를 가져와서 파싱하는 함수가 있다면 실제 코드의 개괄은 다음과 같이 이루어질 수 있다.

let processHTML:(String) -> Void = { ... }
let url = ...
let session = URLSession.shared
let task = session.dataTask(with: url){ data, response, error in 
   // 위 세 파라미터는 모드 옵셔널 타입이다. 응답 데이터를 통해서 HTML 소스문자열을 만든다.
   print("HTTP Response received.")

   guard let data = data, 
   let html = String(data:data, encoding:.utf8)
   else { return }
  
   processHTML(html)
}
task.resume()

델리게이트를 사용해야 할 때

작업 객체를 생성할 때 완료 핸들러를 전달하지 않으면, 응답이 도착했을 때에 시스템이 별도로 생성한 델리게이트가 이를 처리한다. 만약 커스텀 델리게이트를 지정하고 싶다면 세션을 생성할 때 델리게이트를 지정해야 한다. 세션의 델리게이트는 세션의 라이프 사이클 내에서 발생하는 변경에 따라 이를 처리하는 메소드를 제공하면서 동시에 세션 내에서 생성된 작업 객체의 라이프 사이클에 따른 이벤트도 함께 처리한다.

따라서 매 요청에 대한 응답을 델리게이트로 하여금 처리하게 하고 싶다면 델리게이트는 적어도 두 개의 이벤트를 처리할 수 있어야 하는데, 이는 다음과 같다.

  1. 서버로부터 응답 데이터 조각을 받았을 때, 이 데이터를 처리
  2. 서버로부터 모든 응답을 받고 통신이 종료되었을 때 이를 처리.

델리게이트를 사용하면 일정량의 데이터를 수신할 때마다 1이 호출되기 때문에 통신의 전체 진행과정을 표시할 수 있다는 장점이 있다. 단, 시스템의 기본 세션 델리게이트가 해주던 중요한 일 하나를 직접 수행해야 한다. 바로 수신 받은 데이터를 어딘가에 보관하는 일이다. 응답데이터를 누적하여 수집하는 임무는 델리게이트의 일이며, 세션이나 작업 객체는 이에 대한 책임을 지지 않는다.

또 모든 데이터 수신을 완료하고 통신이 종료되었을 때의 처리, 즉 완료 핸들러가 해야하는 일에 대해서도 델리게이트가 직접 처리하게 된다. 위의 두 메소드는 무려 각각 별도의 프로토콜에서 선언되어 있는데, 각각은 다음과 같다.

  1. optional func urlSession(_:dataTask:didReceive:) :: URLSessionDataDelegate
  2. optional func urlSession(_:task:didCompleteWithError:) :: URLSessionTaskDelegate

서버로부터 텍스트를 요청해서 출력하면서, 그 중간에 진행률을 출력하는 동작을 보이기 위한 델리게이트는 이상의 요건을 만족하면서 다음과 같이 작성될 수 있다.

class MySessionDelegate: URLSessionDataDelegate, URLSessionTaskDelegate {
  // 서버로부터 수신받은 데이터를 누적하여 저장할 Data 타입 프로퍼티
  var recievedData = Data()

  // 이 메소드는 데이터 조각이 수신될 때마다 정기적으로 호출된다. 
  // 따라서 수신된 데이터는 기존데이터에 계속해서 연결되어 붙어야 한다. 
  func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didRecieve data:Data) {
    // 데이터를 누적하고
    recievedData.append(contentsOf:data)
    // 전체 데이터양과 지금까지 받은 데이터양
    let totalBytes = dataTask.countOfExpectedToRecived
    let recievedBytes = dataTask.countOfBytesRecived 
    // 혹은 recievedData.count를 사용해도 된다.
    print("Recieved: \(recievedBytes) / \(totalBytes)")

    // 진행률은 위 두 값을 나누어서 계산해도 되지만, dataTask는 progress라는
    // 속성을 가지고 있으므로 이를 사용해도 된다. 
    print("progress: \(dataTask.progress)")
  }

  // 전체 데이터를 모두 수신했을 때의 처리
  // 이 델리게이트 메소드는 URLSessionTaskDelegate에 정의되어 있다. 
  func urlSession(_ session: URLSession, task: URLSessionTask,
                  didCompleteWithError error: Error?) {
    if let string = String(data:recievedData, encoding:.utf8) {
      print(string)
    }
}

이런 식으로 델리게이트를 작성했다면, 세션 객체를 만들 때에는 공유 세션이 아닌 디폴트 세션을 만들고 델리게이트를 지정해준다. 또한 작업 객체를 만들 때에도 완료 핸들러가 없는 버전을 사용해서 생성해야 한다.

let session = URLSession(configuration: URLSessionConfiguration.default,
                         delegate:MySessionDelegate()
                         delegateQueue: nil)
let task = session.dataTask(with: url) // 완료 핸들러 없음
task.resume()
...
session.delegate = nil // 델리게이트는 반드시 제거해야 한다. 

통상 델리게이트를 사용하는 경우에, 델리게이트 프로퍼티는 약한 참조 혹은 소유하지 않는 참조를 사용하는데, 세션 객체의 경우 델리게이트에 대해서 강한 참조를 가진다. 따라서 메모리 누수를 방지하기 위해서 세션의 사용이 끝났을 때, 델리게이트를 제거해 주어야 한다.