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 MYDelegate: NSObject
{
  var data = Data()
  // 수신 받은 데이터 조각을 저장할 공간을 마련한다.
}



extension MYDelegate: URLSessionDataDelegate
{
  // URLSessionDataDelegate는 데이터를 한 번 받아올 때마다 필요한 처리를 한다. 
  func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {

  // 받은 데이터를 누적시키고
  self.data.append(data)

  // 얼마나 받아왔는지를 표시하자.
  let totalBytes = dataTask.countOfExpectedToReceive
  let receivedBytes = dataTask.countOfBytesReceived
  print("\(receivedBytes)/\(totalBytes)")
  }
}



extension MYDelegate: URLSessionTaskDelegate
{
  // URLSessionTaskDelegate는 태스크의 완료시에 취할 행동이다.
  // 받아온 데이터를 문자열로 변환하여 출력한다.
  func urlSession(_ session: URLSession, task: URLSessionTask,
                  didCompleteWithError error: Error?) {
    if let string = String(data:recievedData, encoding:.utf8) {
      print(string)
    }
}

이런 식으로 델리게이트를 작성했다면, 세션 객체를 만들 때에는 공유 세션이 아닌 디폴트 세션을 만들고 델리게이트를 지정해준다. 참고로, delegate 속성은 읽기 전용이기 때문에 다음의 이니셜라이저를 통해서 생성했을 때에만 지정해 줄 수 있다.

let session = URLSession(configuration: .default,
                         delegate:MYDelegate()
                         delegateQueue: .current)
let task = session.dataTask(with: url) // 완료 핸들러 없음
task.resume()
...

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