콘텐츠로 건너뛰기
Home » 오퍼레이션 큐 (Operation Queue)

오퍼레이션 큐 (Operation Queue)

코코아에서 비동기 작업이나 백그라운드 작업을 실행하는 OOP 스타일의 전통적인 방식은 NSOperation 클래스를 사용하는 것이었다. 이 클래스는 실행할 작업을 객체로 감싸면서 백그라운드 실행이나 의존성, 실행 상황에 대한 KVO 등 많은 기능들을 제공해주기는 했지만, 그 자체로는 추상 클래스였기 때문에 이 클래스를 상속하여 커스텀 클래스를 작성해서 사용해야 하는 불편함이 있었다. 이후 Objective-C 2.0에서 코드 블럭이 본격적으로 활용되기 시작하면서 NSBlockOperation 과 같은 코드 블럭을 사용한 간편한 작업 객체 생성이 가능했는데, 우선순위나 의존성 등의 오퍼레이션 객체의 기능이 요구되지 않는 상황에서는 작업 큐 만으로 동기/비동기 작업을 간단히 관리할 수 있게 되었다.

비동기 작업을 위한 큐는 크게 오퍼레이션 큐와 디스패치 큐의 두 종류가 있다. 사실 내부적으로 이 둘은 모두 GCD를 사용하기 때문에(iOS 4.0부터 NSOperationQueue는 GCD를 사용하기 시작했다.) 본질적으로는 같다고 볼 수 있다. 대신에 오퍼레이션 큐는 추가된 코드블럭을 내부적으로 NSBlockOperation으로 만들어서 관리하며, 기존의 오퍼레이션 객체를 추가할 수 있다. 따라서 의존성에 따른 실행 순서를 지정해야 하거나, 특정 타임 아웃 내에 작업을 중단해야 하는 등의 추가적인 관리 기능을 사용할 수 있다.

용도 측면에서 본다면 디스패치 큐는 성능이나 메모리의 오버헤드를 최소화하여 오래 걸리지 않는 작은 규모의 작업을 한꺼번에 실행하면서 CPU 성능을 최적으로 활용하고 싶을 때 사용할 수 있으며, 오퍼레이션 큐는 개별 작업을 관리해야 하고, 시간이 오래 걸릴 수 있는 (그래서 중간에 중지해야 할 수도 있는) 작업을 수행할 때 더 적합하다 하겠다.

오퍼레이션 큐

오퍼레이션 큐를 사용하는 방법은 크게 두 가지이다. 하나는 메인 큐를 사용하는 것이며, 다른 하나는 별도의 작업 큐를 생성하여 사용하는 것이다. 메인 큐는 메인 스레드에서 작동하며, 여러 작업을 동시에 수행하지 않는다. iOS앱에서 메인 스레드는 이벤트 처리와 UI 업데이트를 담당하고 있기 때문에 메인 큐에서는 이러한 처리동작의 사이사이에 남는 틈을 사용해서 큐에 적재된 작업들을 처리한다. 따라서 런루프 모드 위에서 큐가 실행되며, 별도의 스레드를 생성하지 않기 때문에 한 번에 하나의 작업만 처리된다.

이에 비해 커스텀 큐는 별도의 스레드를 생성하며, 몇 몇 프로퍼티에 설정된 값에 따라 한 번에 하나 혹은 여러 개의 작업을 실행할 수 있다. 이 말은 큐 자체가 스레드 풀을 사용하여 여러 스레드를 사용할 수 있다는 것을 의미한다.

큐를 직접 만들기

오퍼레이션 큐를 직접 만드는 것은 의외로 너무 단순한데, 별도의 의존성없이 생성하면 된다. 큐의 동작의 영향을 주는 두 가지 요소는 서비스 품질(Quality of Service)과 최대동시작업 수(max concrruent operation count)인데, 이 값들은 모두 변경 가능한 프로퍼티로 언제든 변경이 가능하다.

다음 예제는 백그라운드 오퍼레이션 큐를 만든 후 여기서 여러 작업을 병렬적으로 처리하는 과정을 보여준다.

let queue = OperationQueue()
queue.qualityOfServce = .background
queue.maxConcurrentOperationCount = 4

for x in stuffs {
    queue.addOperation{ 
        let result = doSomething(x)
        print("The result for \(x) is \(result).")
    }
}

queue.waitUntilAllOperationsAreFinished()

메인 큐를 사용한 작업 실행

메인 큐는 앱의 메인 스레드에서 한 번에 하나의 작업을 실행하는 큐이다. 메인 스레드에서 수행되는 작업은 메인 스레드의 주요 역할인 이벤트 처리 및 UI 업데이트 작업을 쉬는 사이사이에 수행된다. 따라서 메인 큐는 작업들을 런루프 모드에서 실행하게 되며, 한 번에 실행 가능한 작업은 최대 1개로 고정되며, 서비스 레벨 또한 .userInteractive로 고정되어 변경할 수 없다.

var mainQueue = OperationQueue.main
mainQueue.addOperation {
  // ...
}

보통은 백그라운드 큐에서 어떤 작업을 처리한 후 UI를 업데이트하는 코드를 호출해야 할 때 메인 큐를 사용한다. 메인 큐에서 실행되는 동작은 메인스레드에서만 실행되기 때문이다. 다음은 테이블 뷰 셀에 대해서 url 속성이 변경되면, 해당 URL의 이미지를 다운로드 받아서 이미지로 표시하는 예이다.

class FaviconTableViewCell: UITableViewCell {
    var operationQueue: OperationQueue?
    var url: URL? {
        divset {
            var req = URLRequest(URL: self.url!)
            self.textLabel.text = self.url?.host
            NSURLConnection.sendAsynchronousRequest(req, queue:self.OperationQueue!) {
                (resp, data, error) in 
                var image = UIImage(data:data)
                OperationQueue.main.addOperation{
                    self.imageView.image = image
                    self.setNeedsDisplay()
                }
            }
        }
    }
}

디스패치 큐

디스패치 큐는 기본적으로 코드 블럭을 순차적/병렬적으로 실행하는 큐이며, NSOperation과 내부 동작은 유사하기 때문에 사용하는 방법은 크게 다르지 않다. 대신 디스패치 큐는 임의의로 생성하기 보다는 서비스 레벨에 맞는 전역큐를 사용하는 것이 권장되며, 특수한 경우에도 시스템이 관리하는 전역 큐를 타깃으로 한 디스패치 큐를 사용하는 것이 좋다고 한다.

DispatchQueue.global(.background).async{ weak self in 
    // ... 백그라운드에서 실행할 작업...
    self.textLabel.text = self.url!
    URLSession.dataTask(with:self.url!){ (data, resp, error) in 
        guard let data = data, let image = UIImage(data:data) 
        else { return }
        DispatchQueue.main.async{
            self.imageView.image = image
            self.setNeedsDisplay()
        }
    }
}