Swift - NSOperationQueue 병렬 프로그래밍 기초

NSOperationQueue는 큐에 작업을 쌓아두고 이 작업들을 병렬로 처리하기 위한 클래스이다. 큐(Queue)라고 해서 한번에 하나씩 처리가 될 거라고 생각될 수도 있겠지만, 개별 작업을 개별 스레드에서 가능한 만큼 병렬로 한번에 실행시키려고 하는 기특한 녀석이다.

NSOperation

우선은 NSOperation 에 대해 알아야 한다. 이 클래스는 오퍼레이션큐에 넣기 위한 단위 클래스이다. 즉 모든 작업은 이 NSOperation 타입의 클래스로 구현되어야 한다. 다만 기본적으로 이 클래스는 추상클래스에 가까워서 단일로는 아무일도 하지 않고 상속해서 구현하는 형태로 사용해야 한다.

아래는 NSOperation을 상속받은 MyOperation 이라는 클래스 예제이다.
class MyOperation: NSOperation {
    var index: Int?
    override func main() {
        println("From My Operation \(self.index)")
    }
    init(index: Int) {
        super.init()
        self.index = index
    }
}
여기서 main() 이라는 메소드가 오버라이드 되어 있는 것을 알 수 있는데 이 메소드가 바로 실제로 병렬로 작동할 코드이다.

NSOperationQueue

이제 실제로 큐를 만들어서 병렬 처리를 해 볼 차례다.
class MyWork {
    let queue = NSOperationQueue()
    init() {
        self.queue.addOperation(MyOperation(index: 0))
        self.queue.addOperation(MyOperation(index: 1))
        self.queue.addOperation(MyOperation(index: 2))
        self.queue.addOperation(MyOperation(index: 3))
        self.queue.addOperation(MyOperation(index: 4))
    }
}
이런 클래스를 만들어 아래와 같이 인스턴스를 만들면 병렬작업이 시작된다.
let work = MyWork()
코드 내용으로 봐선 콘솔에는 지정된 인덱스의 번호의 글자들이 출력될거라고 예상된다. 그런데 실제로 테스트 해 봤을때는 아래와 같은 결과가 찍혔다.
FFFFFFrrrrrroooooommmmmm      MMMMMMyyyyyy      OOOOOOppppppeeeeeerrrrrraaaaaattttttiiiiiioooooonnnnnn      302451
예상 가능하신 분들도 있겠지만, println()이 한 글자(Character) 단위로 글자를 출력하고 이게 병렬로 처리되어서 모든 MyOperation이 한번에 동작하는 것임을 알 수 있다. 뭔가 이상하지만 오히려 병렬 처리가 잘 된다고 볼 수 있다.

NSOperation의 프로퍼티와 오버라이드용 메소드들

여기서 소개하는 프로퍼티와 메소드는 극히 일부분에 해당한다. 상세한 것은 레퍼런스매뉴얼을 참고하자.

start()

start() 라는 메소드는 main()이 실행되기 전에 호출되는 메소드로 실제 작업 시작 전의 준비과정이 필요한 것들을 오버라이드 해서 구현하면 된다. 구현 시 절대로 super에 뭔가를 호출해서는 안된다는 제약이 있다.

start() 메소드는 필수는 아니므로 필요없으면 구현하지 않아도 된다.

cancel()

cancel() 이라는 이름에서 볼 수 있듯이 취소한다는 이름의 메소드다. 하지만 이 메소드를 호출한다고 해서 작업이 취소되는건 아니다. cancel() 메소드의 실제 역활은 작업이 취소되었을 때 호출되는 메소드다. 따라서 취소 시 하고싶은 작업을 여기다 기술하면 된다.

cancel() 역시 필수가 아니므로 필요없다면 구현하지 않아도 된다.

cancelled

cancelled는 메소드가 아닌 프로퍼티이다. 이름에서 볼 수 있듯이 실행이 중지되면 자동으로 true로 세팅된다. 만약 작업(main) 내에서 루프를 사용하고 있다면 이 플래그를 체크해서 루프를 중지시켜야 한다.

참고로 이 프로퍼티는 OS X 10.10 요세미티부터 지원된다. iOS는 어떻게 될지 모르겠지만 기존 Objective-C를 쓰던 때에는 isCancelled() 라는 메소드가 동일한 역활을 한다.

NSOperationQueue의 프로퍼티와 메소드들

addOperation(operation: NSOperation!)

앞서 본 예제에서 볼 수 있듯이 실제 오퍼레이션(NSOperation을 상속받은 클래스의 인스턴스)을 큐에 추가하는 담당이다. 추가하자 마자 바로 작업이 실행된다.

비슷한 이름을 가진 메소드가 몇 가지 있는데 찾아보면 유용할지도 모른다.

cancelAllOperations()

이름처럼 모든 오퍼레이션을 취소시키는 역활이다. 하지만 이것 만으로 반드시 모든 오퍼레이션이 취소되는 것은 아니다. 앞서 소개했듯이 NSOperation 자체에서도 cancelled 프로퍼티를 체크해서 종료하도록 처리를 해 놔야만 실제로 작업이 종료된다.

maxConcurrentOperationCount

이름에서 볼 수 있듯이 동시에 처리된 오퍼레이션의 갯수를 정할 수 있다. 즉 생성시킬 스레드 갯수를 의미한다고도 볼 수 있다. Int 타입으로 원하는 값을 넣으면 된다. 하지만 특별히 제한할 필요가 없다면 기본값으로 놔둬도 된다.

기본값은 NSOperationQueueDefaultMaxConcurrentOperationCount 이다. 이 값의 의미는 '시스템 컨디션에 따른 최대' 값이다. 의외로 애플에서는 이 값을 권장한다.

NSOperation의 다른 예제

이번에는 NSOperation 클래스 자체가 아니라 다른 팩토리 클래스를 살펴볼까 한다. 대충 NSOperation 의 양산형 자식 클래스라고 생각하자. -_-

NSInvocationOperation

NSInvocationOperation 은 NSOperation의 자식클래스로, 이미 구현되어 있는 특정 클래스의 특정 메소드를 작업메소드(즉 위 NSOperation의 main와 동일)로 쓰고자 할 때 사용된다.
class MyAnotherOperation: NSObject {
    var index: Int?
    var operation: NSOperation {
        return NSInvocationOperation(target: self, selector: "work", object: nil)
    }
    
    func work() {
        println("From My Another Operation \(self.index)")
    }
    
    init(index: Int) {
        self.index = index
    }
}

class MyAnotherWork {
    let queue = NSOperationQueue()
    
    init() {
        self.queue.addOperation(MyAnotherOperation(index: 0).operation)
        self.queue.addOperation(MyAnotherOperation(index: 1).operation)
    }
}
MyAnotherOperation 클래스가 NSObject를 상속받은건 셀렉터를 사용하기 위한 환경을 만들어 주기 위함이다. NSObject를 상속받지 않으면 셀렉터를 찾지 못 하기 때문이다.

MyAnotherOperation의 operation 프로퍼티는 getter 로써 핵심적인 역활을 한다. NSInvocationOperation 클래스를 이용해 work 라는 메소드를 작업메소드로 사용한다고 설정해서 인스턴스를 생성해서 리턴하는 것이다.

그래서 MyAnotherWork 라는 오퍼레이션큐에 addOperation을 하는 곳에서도 이 operation 프로퍼티를 받는 것을 볼 수 있다.

NSBlockOperation

역시 NSOperation의 자식클래스이다. 이름으로는 Objective-C의 블럭(Block)을 연상하게 되는데 그게 맞다. 단지 스위프트에서는 클로져를 넘기는 식으로 구현해야 한다.
class SomeWork {
    let queue = NSOperationQueue()
    
    init() {
        let operation1 = NSBlockOperation(block: {
            println("Operation 1")
        })
        self.queue.addOperation(operation1)

        let operation2 = NSBlockOperation(block: {
            println("Operation 2")
        })
        self.queue.addOperation(operation2)
    }
}
이번에는 별도의 작업클래스의 선언이 필요없다는 것에 편리함을 느끼는 분들도 있을 것 같다. 이름처럼 블럭 아니 스위프트에선 클로져를 넘겨주는 방식으로 쉽고 간단하게 NSOperation 타입의 인스턴스를 생성 할 수 있다.

기타

방식은 달라도 이 오퍼레이션(NSOperation)은 스레드에서 동작한다. 따라서 NSThread에서 제공되는 기능을 활용할 수 있다. 예를 들어 루프에서 CPU점유율을 떨어뜨리기 위해 sleep을 사용하고 싶을 때가 있다. 이럴때는 NSThread에서 쓰던 동일한 메소드를 활용 할 수 있다.
// 0.001초간 sleep
NSThread.sleepForTimeInterval(0.001)
참고하자.

[돌아가기] Swift - 병렬 프로그래밍(Concurrency Programming) 가이드
[관련글] 스위프트(Swift) 가이드

댓글

익명님의 메시지…
개념잡는데 많은 도움이 되었습니다.

감사드립니다.

이 블로그의 인기 게시물

소수점 제거 함수 삼총사 ceil(), floor(), round()

버전(Version)을 제대로 이해하기