Using NSURLSession

NSURLSession 사용하기

NSURLSession은 NSURLConnection을 대체하기 위해 나온 새로운 클래스로 보다 사용하기 쉽고, 더 강력한 기능(백그라운드 다운로드/업로드)을 지원한다.

간단 사용방법

HTTP를 통해서 웹으로부터 데이터를 받아오는 경우를 생각해보자.

NSData(contentsOfURL:)을 사용할 수도 있는데, 이는 동기 방식으로 동작하는 것이고, NSURLSession은 항상 비동기로 동작한다.

  1. NSURLSession 객체를 생성한다. 기본적으로 데이터 교환은 디폴트 설정으로 해도 된다. 필요한 경우 델리게이트를 추가할 수 있다.
  2. 생성한 세션으로부터 URL 및 완료 핸들러를 주고, NSURLSessionTask 객체를 생성한다. 완료 핸들러의 경우에는 델리게이트를 사용하는 경우라면 주지 않아도 된다.
  3. task.resume() 을 호출한다. 완료시 완료핸들러가 호출되는데, 완료핸들러가 없는 경우에는 세션의 델리게이트 메소드가 호출될 것이다.

다음은 네이버로부터 로또 당첨번호를 받아와서 화면에 출력하는 코드이다.

import Foundation

func getHTML(n:Int, handler:([String]) -> Void) {
    let baseAddress = "http://search.naver.com/search.naver?"
    let params = "sm=tab_drt&where=nexearch&query=\(n)회로또"
    let encodedParams = (params as NSString)
        .stringByAddingPercentEncodingWithAllowedCharacters(
                .URLHostAllowedCharacterSet())! as String
    let url = NSURL(string: baseAddress + encodedParams)!

    let session = NSURLSession(configuration:
        NSURLSessionConfiguration.defaultSessionConfiguration())
    let task = session.dataTaskWithURL(url){
        _data, _response, _error in 
        if let data = _data, let _ = _response,
            let html = NSString(data:data, encoding: NSUTF8StringEncoding),
            let regex = try? NSRegularExpression(pattern:"ball(\\d+)", options:[])
        {
            let matches = regex.matchesInString((html as String), options:[],
                    range:NSMakeRange(0, html.length))
            let results = matches.map{ html.substringWithRange($0.rangeAtIndex(1))}
            handler(results)
        } else {
            print("error")
        }
    }

    task.resume()
}

getHTML(111){ 
    print($0)
    CFRunLoopStop(CFRunLoopGetMain())
}

CFRunLoopRun()

완료 핸들러를 따로 지정하지 않으면 NSURLSession 객체는 데이터를 다 받은 후에 시스템이 제공하는 기본 델리게이트에게 델리게이트 메소드를 호출할 것이다. 만약 어떤 처리를 해야 한다면, (응답을 받았으니 확인을 해야지) 커스텀 델리게이트를 사용해야 한다.

let sessionDelegate = <# ... #>
let session = NSURLSession(configuration: .defaultSessionConfiguration(),
                        delegate: sessionDelegate,
                        delegateQueue: NSOperationQeueu.currentQueue())
let task = session.dataTaskWithURL(url)
task.resume()

델리게이트는 최소 다음 두 개의 메소드를 구현하고 있어야 한다.

  • -URLSession:dataTask:didReceivedData: in NSURLSessionDataDelegate
  • -URLSession:task:didCompleteWithError: in NSURLSessionDataTaskDelegate

왜냐면 URLSession 객체는 데이터를 쪼개서 받게 되고, 이 때 각 조각을 모아서 보관하는 것은 델리게이트의 책임이기 때문이다. (완료시 호출되는 경우에 따로 데이터를 인자로 넘겨주지 않고 있다.)

따라서 대략의 델리게이트 코드는 다음과 같은 식어야 한다.

class MYSessionDelegate: NSObject, NSURLSessionDataDelegate, NSURLSessionTaskDelegate {
    var data = NSMutableData()

    init(){
        super.init()
    }

    //  작업이 완료되었을 때 처리. 
    //  데이터는 따로 전달받지 않는다. 따라서 데이터는 보관하고 있어야 한다. 
    //  기본 델리게이트는 이를 저장해두었다가, 완료핸들러가 있으면 이를 호출해주는 식.
    func URLSession(_ session: NSURLSession, task: NSURLSessionTask,
            didCompleteWithError error:NSError?) {
        if let e = error {
            println("There was an error.")
        } else {
            let source = NSString(data:data, encoding:NSUTF8Encoding)!
            ....
            println("\(n): \(result)")
        }
    }

    //  데이터 조각을 받을 때마다 이를 저장해줘야 한다.
    func URLSession(_ session: NSURLSession, dataTask: NSURLSessionDataTask,
            didReceiveData data:NSData) {
        println("Received \(data.length) bytes from remote host")
        self.data.appendData(data)
    }
}

폼데이터 전송등을 위해서 POST 요청을 보내는 것도 가능하다. 이 경우에는 method나 헤더 정보가 있어야 하므로 별도의 request payload가 정의되어야 하며, 이는 NSURLRequest를 만들어서 쓸 수 있다.

NSURLRequest 사용하기

NSURLRequest는 주로 +requestWithURL:cachePolicy:timeoutInterval: 등의 옵션을 추가하여 요청 데이터를 생성하는데 쓰이는데, 헤더필드를 정의하는 것도 가능하다. POST 방식을 쓰려면 NSMutableURLRequest를 써야 한다.

  • .HTTPMethod : “GET”, “POST” 등 메소드를 설정한다.(기본은 GET)
  • .HTTPShouldHandleCookies
  • -addValue:forHTTPHeaderField: 헤더필드값을 추가한다. 이미 추가한 필드값에 대한 변경은 -setValue:forHTTPHeaderField:를 쓴다. 이 때 각 값은 String? 포맷이다.

기존에 NSURLConnection을 이용하여 POST 방식으로 파일을 업로드하기 위해서는 boundary라는 필드가 헤더에 있어야 하는데, 이 값은 HTTP Body 첫줄과 마지막에 다음과 같이 나온다.

--{boundary값}
... data ...
--{boundary값}--
(각 개행은 \r\n)

또한, 이런 경우 타입은 application/x-www-multipart-formdata 타입으로 올라간다.

업로드의 경우에는 HTTP BODY에 데이터를 채워넣는 수고를 덜기 위해서 NSURLSessaionUploadTask를 쓸 수 있다.

func uploadTaskWithRequest(_ request: NSURLRequest,
                            fromFile fileURL:NSURL,
                        completionHandler completionHander: (NSData!, NSURLResponse!, NSError!) -> Void)?) -> NSURLSessionUploadTask

이는 NSURLSessionDataTask의 서브 클래스로 파일 업로드를 위한 작업을 조금 더 쉽게 만들어준다. HTTP POST의 파일 업로드는 데이터를 멀티파트 폼데이터나 URL인코드방식으로 인코딩하고, (인코딩 방식도 헤더에 명시해야 수신측에서 받을 수 있다.) HTTP BODY에 인코딩된 데이터를 실어서 보내게 된다. 이 구조를 알고 있다면 간단한 업로드 작업은 다음과 같이 구성할 수 있다.

let request = NSURLMutableRequest(url:serverURL)
request.HTTPMethod = "POST"
let session = NSURLSession.sharedSession()
let task = session.uploadTaskWithRequest(request, fromFile:fileURL){
    data, response, error in
    if error != nil {
        println("Fail to upload")
    } else {
        println("Successfully uploaded.")
    }
}

task.resume()

수신측 서버는 HTTP BODY를 (이진 데이터가 그대로 들어오므로) 그대로 디스크 버퍼에 써서 파일로 만들면 된다.

업로드의 경우 파일 URL을 써도 되고, DATA로 만들어서 보낼 수도 있다. (대신 이 때 데이터를 만든 인코딩을 수신측에서도 알아야 다시 파일로 복구할 수 있음은 당연하다.) 어쨌든 업로드작업(NSURLSessionUploadTask) 객체를 사용하면 파일을 데이터로 변환해서 바운더리 서식을 붙이고, HTTP BODY의 길이를 미리 계산하는 등의 복잡한 작업을 생략할 수 있다.

또 데이터 작업과 달리 업로드는 백그라운드에서 (즉 앱이 백그라운드에 들어가도) 별도의 프로세스에서 진행될 수 있다.

델리게이트 메소드들은 다음과 같은 것들이 있음.

  • URLSession:task:didCompleteWithError::
  • URLSession:task:didreceiveChallegne:completionHandler::
  • URLSession:task:didSendBodyData:totalBytesSent:totalBytesExpectedToSend::
  • URLSession:task:needNewBodyStream:: 마지막은 완료핸들러
  • URLsesson:task:willPerformHTTPRedirection:newRequest:completionHander::

동기식으로 데이터를 받기

동기식으로 데이터를 받을 때에는 굳이 URL Loading System을 쓸 필요가 없다.

  • NSData(contentsOfURL:error:)
  • NSString(contentsOfURL:encoding:error:)
  • NSImage(contentsOfURL:)
  • UIImage(contentsOfFile:), UIImage(data:)
  • NSSound(contentsOfURL:byReference:), NSSound(data:)

파일과 관련있는 이런 클래스들에서는 파일이나 URL로부터 객체를 생성하는 기능이 있는데, 이 때 URL은 file: 프로토콜 외에도 HTTP도 지원한다.