Tag Archives: GCD

iOS NSOperation과 NSOperationQueue 이해하기

iOS/Android가 처음나왔을때 접할 수 있었던 앱들의 수준이 기억납니다. 지금 생각하면 매우 조악하고 허접한 앱들이 많이 있었습니다. 특이 무언가 버튼 하나 누르면 결과가 나올때까지 화면의 전체 UI가 정지 된다던지 사용성이 떨어지는 앱들이 많이 있었는데 요즘은 이런 바보같은 앱들이 보기 힘들어진것 같습니다.

대부분의 앱들이 쓰레드를 적절히 활용하고 있고 부담이 될 수 있는 작업들은 백그라운드에서 실행되도록 할 수 있습니다. 일반적으로 iOS 개발자들이 이러한 쓰레드 활용을 위해서는 Grand Central Dispatch와 NSOperation 두가지를 활용할 수 있습니다. 이 글에서는 우선 NSOperation에 대해서 다루어 보겠습니다.

NSOperation은 어떤 하나의 작업을 나타냅니다. NSOperation은 모델링 상태, 우선순위, 의존성 그리고 관리를 지원하는 유용하고 Thread safe한 추상 클래스입니다.

NSOperation를 상속하여 커스텀 클래스를 만들어 활용하는것이 이해가 되지 않을 경우 NSBlockOperation 또는 NSInvocationOperation을 활용하시기 바랍니다.

예를 들어 네트워크 요청, 이미지 리사이즈, 텍스트 처리, 또는 기타 다양한 반복처리등 오래 걸리는 작업을 처리해야하는 NSOperation이 있다고 할 때, 이 특정 작업이 담겨있는 객체는 감독 없이 많은 일을 할 수 없습니다. 이러한 작업을 진행하는 감독을 NSOperationQueue가 담당합니다.

NSOperationQueue

NSOperationQueue는 작업의 동시 실행을 조절하는 일을 합니다. 이 큐는 우선순위 큐와 같이 동작하며 기본적으로 First-In-First-Out으로 동작을 하나 더 높은 우선 순위(NSOperation.queuePriority)의 작업이 들어오게 되면 더 낮은 우선 순위를 가진 작업들을 건너뛰고 실행되게 됩니다. NSOperationQueue는 maxConcurrentOperationCount 프로퍼티 설정을 이용해 주어진 특정 시간동안 동시에 실행 가능한 작업의 숫자를 제한하는것이 가능합니다.

NSOperation을 시작하기 위해서는 start 메소드를 호출하거나 NSOperationQueue에 추가하여 큐의 가장 처음에 도달하면 한번 실행되도록 할 수 있습니다. NSOperation을 사용하여 얻을 수 있는 많은 장점들이NSOperationQueue를 사용함에서 오기 때문에 NSOperation의 start를 직접적으로 호출하는것보다는 NSOperationQueue에 추가하여 사용하는것이 바람직합니다.

State

NSOperation 의 작업 실행 과정을 설명하기 위해 상태 머신을 통해 표현해 보자면 다음과 같습니다.

ready → executing → finished

명시적인 state 프로퍼티가 존재하는것 대신에 state는 암묵적으로 해당 키패스에 KVO(Key-Value Observing) 통지를 하는것으로 결정하게 됩니다. 작업이 실행될 준비가 되었다면 ready 키패스에 KVO 통지를 하면 이에 대응하는 프로퍼티가 true를 반환하게 됩니다.

  • ready: 이 값이 true를 반환한다면 작업이 실행될 준비가 되었음을 의미합니다. false라면 작업의 초기화가 아직 끝나지 않은것을 의미합니다.
  • executing: 이 값이 true를 반환한다면 해당 작업이 현재 작업중임을 의미합니다. false라면 그 외의 상황임을 의미합니다.
  • finished: 이 값이 true를 반환한다면 작업이 성공적으로 종료되었거나 취소가 되었음을 의미합니다. NSOperationQueue는 이 finished 값이 true로 바뀌지 않는한 큐에서 Dequeue 하지 않으므로 데드락을 야기할 소지가 있습니다. 그러므로 작업을 구현할 때 주의해 주시기 바랍니다.

Cancellation

필요없는 작업이 실행되는것을 방지하거나 의존성 있는 작업이 실패하였을때, 혹은 유저에 의해 명시적으로 취소가 될때 작업을 취소하는것은 유용하게 사용됩니다.

state값이 변경되는 과정과 비슷하게 작업이 취소되면 NSOperation은 KVO를 통해 cancelled 키패스에 통지를 보내게 됩니다. 작업이 취소되었다면 내부의 사용되던 자원들을 정리하고 가능한 빠르게 적절한 최종상태로 변경하게 됩니다. cancelled와 finished 프로퍼티의 값은 true로 바뀌고 executing 프로퍼티의 값은 false로 변경됩니다.

Priority

모든 작업들은 같은 중요성을 가지지 않을것입니다. queuePriority 프로퍼티의 값을 설정함으로써 NSOperationQueue가 우선순위를 상승 또는 하강하게 됩니다.

// Objective-C
typedef enum : NSInteger {
   NSOperationQueuePriorityVeryLow = -8,
   NSOperationQueuePriorityLow = -4,
   NSOperationQueuePriorityNormal = 0,
   NSOperationQueuePriorityHigh = 4,
   NSOperationQueuePriorityVeryHigh = 8
} NSOperationQueuePriority;

// Swift
enum NSOperationQueuePriority : Int {
    case VeryLow
    case Low
    case Normal
    case High
    case VeryHigh
}

Asynchronous Operations

iOS8/OSX요세미티에서 concurrent 프로퍼티가 Deprecate되고 새로운 asynchronous 프로퍼티가 추가되었습니다. 원래 concurrent 프로퍼티는 하나의 메인 메소드에서 모든 작업을 수행할것인지 여부를 구별하는데 사용되고 작업이 비동기적으로 수행될 때 그 상태를 관리하는데 사용되었습니다. 이 프로퍼티는 NSOperationQueue가 다른 별도의 쓰레드에서 작업을 실행할지 여부를 결정하는데 사용되었습니다. NSOperationQueue가 기존의 직접적으로 쓰레드를 관리하는 방법에서 Internal Dispatch Queue 위에서 동작하도록 변경된 이후 이 프로퍼티는 무시하게 되었습니다. 새로운 asynchronous 프로퍼티는 기존의 복잡한 의미를 갖는 concurrent 프로퍼티 대신에 NSOperation이 동기로 실행될지 비동기로 실행될지를 결정하는 유일한 방법입니다.

Dependencies

어떤 복잡한 어플리케이션에서는 큰 작업을 다양한 작은 작업으로 분리하여 처리할 수 있습니다. 이러한 처리를 NSOperation의 의존성을 통해 할 수 있습니다.

예를들어 서버로부터 이미지를 다운로드 받고 리사이즈하는 작업이 있다고 할 때 네트워크 통신을 하는 작업 한개, 이미지를 리사이즈하는 작업 한개로 분리할 수 있습니다. 그러나 이미지 다운로드가 완료될 때 까지 리사이즈 작업은 수행할 수 없습니다. 네트워크 작업은 리사이즈작업에게 의존성을 가지게 됩니다. 네트워크 작업이 완료되어야 리사이즈 작업이 시작될 수 있습니다.

// Objective-C
NSOperation *networkingOperation = ...
NSOperation *resizingOperation = ...
[resizingOperation addDependency:networkingOperation];

NSOperationQueue *operationQueue = [NSOperationQueue mainQueue];
[operationQueue addOperation:networkingOperation];
[operationQueue addOperation:resizingOperation];

// Swift
let networkingOperation : NSOperation = ...
let resizingOperation : NSOperation = ...
resizingOperation.addDependency(networkingOperation)

let operationQueue = NSOperationQueue.mainQueue()
operationQueue.addOperations([networkingOperation, resizingOperation], waitUntilFinished: false)

위의 작업은 의존성을 가지고 있는 작업들의 finished값이 true가 될때까지 시작되지 않습니다. 만약 A가 B의 의존성을 가지고 있고 반대로 B가 A의 의존성을 가지게 되면 의존성 사이클이 생기게 되는데 데드락을 발생시킬 수 있으니 주의하도록 합니다.

completionBlock

NSOperation이 종료되면 completionBlock이 한번 실행됩니다. 이는 모델이나 뷰컨트롤러에서 작업이 수행될때 부가적인 작업을 수행할 수 있도록 하는 방법을 제공합니다.

// Objective-C
NSOperation *operation = ...;
operation.completionBlock = ^{
    NSLog("Completed");
};

[[NSOperationQueue mainQueue] addOperation:operation];

// Swift
let operation = NSOperation()
operation.completionBlock = {
    println("Completed")
}

NSOperationQueue.mainQueue().addOperation(operation)

예를 들어 네트워크를 통해 서버로부터 어떤 데이터를 다운받은 다음에 응답값을 처리하기 위해서 적절하게 사용될 수 있습니다.

When to Use NSOperation

NSOperation은 각 작업들간에 의존성을 설정할 수 있고 우선순위 큐와 QOS를 통해 작업을 수행할 수 있습니다. GCD 큐와 다르게 NSOperation은 작업을 취소하거나 현재 상태를 질의할 수 있습니다. 또한 NSOperation을 상속하여 나중에 다른곳에서 참조가능하도록 결과들을 보관하는것이 가능합니다.

참고: http://nshipster.com/nsoperation/

[iOS] GCD를 이용한 싱글턴 클래스 구현

GCD(Grand Central Dispatch)을 이용하여 Singleton 패턴을 구현하는 방법을 정리해 봅니다. GCD에서 제공하는 dispatch_once는 단한번만 실행되는 코드 블록을 지정할 수 있습니다. GCD는 쓰레드 세이프하고 OS가 모든것을 관리해준다는 장점이 있습니다.

MyObject.h

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

+ (instancetype)getInstance;

@end

 MyObject.m

#import "MyObject.h"

@implementation MyObject

+ (instancetype)getInstance {
    static MyObject *_instance = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _instance = [[MyObject alloc] init];
    });
    return _instance;
}

@end

.h 파일에서는 getInstance라는 클래스 메소드를 추가하였고 .m에서는 그 실체를 구현하였습니다.

위에서 중요한것은 dispatch_once_t와 dispatch_once의 상관관계입니다. dispatch_once_t는 dispatch_once가 실행되었는지 여부를 저장하고 있는 포인터 변수입니다. 이 값을 보존하지 못한다면 여러번 실행될 수도 있다는 것이 되겠네요.

추가로 기존의 방식으로 싱글턴을 구현하려면 @synchronized를 활용하는 방법이 있습니다.

#import "MyObject.h"

@implementation MyObject

+ (instancetype)getInstance {
    static MyObject *_instance = nil;
    if (_instance == nil) {
        @synchronized (self) {
            if (_instance == nil)
                _instance = [[MyObject alloc] init];
        }
    }

    return _instance;
}

@end

참조 : Apple GCD Reference