작업큐

작업 큐

때때로, 그리고 생각보다는 자주 프로그램은 한 번에 하나 이상의 일을 동시에 처리해야 하는 경우가 있다. 이러한 것 중 가장 중요한 것으로는 사용자의 입력에 대해서 반응하는 것이다. 그리고 그와 동시에 네트워크로 통신을 하거나, 대용량의 데이터를 읽거나 쓰고 혹은 데이터들을 처리하는 것을 동시에 진행한다.

앱의 최우선순위는 사용자에게 반응할 수 있게 하는 것이다. 어떤 장시간이 걸리는 일에 대해서 지속적으로 피드백이 주어진다면 사용자들은 그것을 기다릴 수 있다. 하지만 그렇지 않다면 (그래서 UI가 얼어버린 것으로 보인다면) 사용자들은 앱이 문제가 있거나 디바이스가 느리다고 인지하게 된다.

앱의 두 번째 우선순위는 가용한 리소스는 놀리지 않고 열심히 쓰는 것이다. 작업 큐는 이 두 가지 토끼를 동시에 잡기 위한 하나의 도구이다. 작업큐는 NSOperationQueue의 인스턴스로 작업들-그러니까 일련의 코드 조각들-을 관리한다. 하나의 큐에서는 하나 이상의 작업이 동시에 수행될 수 있고, 메인 큐를 위시하여 적어도 하나 이상의 큐는 항상 존재한다. 지금까지 작업큐를 명시적으로 썼든 그렇지 않든 대부분의 코드는 메인 큐 내에서 실행되었을 것이다. GUI와 관련된 작업은 늘 메인 큐에서 일어났기 때문에 메인 큐에서 시간이 많이 걸리는 작업을 하게되면 GUI가 느려지거나 얼어버리기 시작한다.

작업 큐는 스레드와는 다른 것이다. 여러가지 개념적 측면에서 스레드와 비슷하기는 하지만, 작업 큐는 하나 혹은 그 이상의 스레드를 사용하며, 그 관리는 시스템이 알아서 한다.(그렇다, 이것은 GCD의 OOP판이다.) 스레드는 자원측면이 강조된 동시성 처리 방법이지만 작업 큐는 훨씬 더 간단하고 쉬운 방법으로 효율적으로 처리된다.

작업 큐는 시스템의 가용 자원에 대해서 알고 있기 때문에 만약 새로운 작업큐를 만들고 여기에 작업을 투입하면, 시스템은 자동적으로 가용 자원을 분배한다. 멀티 코어, 멀티 CPU 시스템이라면 작업큐는 알아서 각 CPU 혹은 코어간의 노동강도(?)의 균형을 맞추어 동작한다. 최근 출시되는 대부분의 애플 제품은 2개 혹은 그 이상의 코어를 가지고 있기 때문에 작업 큐를 사용하는 것은 동시성처리에서 쉽게, 큰 성능 향상을 얻을 수 있다.

작업큐와 NSOperation

단순히 작업 큐는 들어온 순서대로 작업을 실행한다. 이 때 작업은 NSOperation의 인스턴스이며, 수행되어야 할 작업을 객체화한 것이다. NSOperationQueue-addOperationWithBlock:이라는 메소드가 있어서 작업 객체를 굳이 만들지 않더라도 블럭(클로저)를 이용해서 쉽게 병렬처리 작업을 추가(하고 바로 시작)할 수 있다.

var mainQueue = NSOperationQueue.mainQueue()
mainQueue.addOperationWithBlock {
    // ...
}

메인 큐는 메인스레드에서 동작한다. 백그라운드에서 돌아가는 다른 큐를만들고 싶다면, 그냥 NSOperationQueue 객체를 하나 추가하고 여기에 작업을 넣으면 된다. 작업 큐를 만드는 것이 반드시 스레드를 만드는 것과 이어지지는 않는다. 스레드를 새로 만드는 작업은 시스템 측면에서는 매우 비용이 많이 들어가는 작업이므로, 시스템은 가능한한 기존 스레드를 재활용하려 할 것이다.

중요한 것은 GUI의 업데이트는 반드시 메인큐에서 해야 한다는 것이다. 따라서 백그라운드 큐에서 GUI를 업데이트하려면 다시 메인큐로 작업을 보내줄 필요가 있는데, 이 역시 매우 간단하다.

let backgroundQueue = NSOperationQueue()
backgroundQeueu.addOperationWithBlock{
    // ... do something in backgroudn.
    // and update GUI
    let mainQueue = NSOperationQueue.mainQueue()
    mainQueue.addOperationWithBlock {
        // update GUI.
    }
}

이를 하나로 합쳐서 URL 속성이 변경되면 파비콘을 업데이트해주는 테이블 뷰 셀을 생각해보자. HTTP 통신을 위해서 NSURLSession을 사용할 수도 있는데, 여기서는 그냥 NSURLConnection을 쓰도록 한다.

class FaviconTableViewCell: UITableViewCell {
    var operationQueue: NSOperationQueue?
    var url: NSURL? {
        didSet {
            var request = NSURLRequest(URL: self.url!)
            self.textLabel.text = self.url?.host

            NSURLConnection.sendAsynchronousRequest(request,
                queue:self.operationQueue!,
                completionHandler: {
                    response, data, error in
                    var image = UIImage(data:data)
                    NSOperationQueue.mainQueue().addOperationWithBlock {
                        self.imageView.image = image
                        self.setNeedsLayout()
                    }
                }
            )
        }
    }
}

위 코드에서 +sendAsynchronousRequest:queue:completionHandler:에서 queue는 완료 핸들러가 동작할 큐를 의미한다. nil을 보내면 메인큐에서 동작한다.