Swift – BlockOperation 사용하기

Operation(NSOperation)은 특정한 작업을 수행하기 위한 코드와 데이터를 감싸는 객체로 해당 작업을 동기/비동기로 필요한 시점에 실행하기 위한 용도로 사용한다. NSOperation은 OSX 10.5에서도입되었는데, 이 시점의 Objective-C에서는 클래스의 외부에서 코드를 주입할 수 있는 언어적인 장치가 존재하지 않았기 때문에, 이를 사용하려면 무조건 NSOperation의 서브클래스를 만들어야 하는 불편한 점이 있었다. 따라서 간단한 코드 조각을 실행하기 위해서 NSOperation을 사용하는 것은 꽤나 불편한 일이었다.

OSX 10.6(스노우레퍼드)부터 GCC/Clang을 기본 컴파일러로 사용하게 되었고 이때부터 코드블럭 기능이 지원되었다. 즉 별도의 서브클래스를 작성하지 않더라도 코드 블럭의 형태로, 마치 클로저처럼 코드와 코드가 캡쳐링하는 데이터를 주입하여 오퍼레이션 인스턴스를 만드는 것이 가능해졌다. 실질적으로 NSOperation을 그대로 사용해야할 이유는 현재로서는 찾아보기 힘들다.

Swift – BlockOperation 사용하기 더보기

작업큐

작업 큐

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

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

[iOS/OSX] 특정 작업을 병렬로 처리하기

“동시에 진행되는 작업”을 처리하기 위해서는 iOS 및 OSX 환경에서는 크게 두 가지 방법을 (흔히) 사용한다. GCD (dispatch queue)와 Operation Queue가 그것이다. 오퍼레이션 큐는 GCD의 Objective-C 버전이라 할 만큼 비슷한데 (사실 좀 다르기는 다르다) 어쨌거나 이 두 가지 방법은 스레드의 생성과 관리를 시스템이 알아서 처리해주는 레벨로 가지고 내려가기 때문에 실제로 프로그래머가 신경써야 할 부분을 “동시에 진행되는 작업을 처리”하는 부분에만 집중하면 되도록 해준다.

예를 들면 네트워크를 통해 데이터를 로드해야 하는 경우나 그 반대로 네트워크를 통해 데이터를 저장해야 하는 경우에 응답이 느리다면 (이는 디스크 같은 영구 저장소를 액세스할 때도 일어날 수 있다. 아주 미묘한 수준이기는 하나 이런 작업은 앱에 blocking을 가져오고 UI에 대한 반응을 느리게 만든다) 이 작업의 처리를 기다리는 동안 앱은 사용자의 터치에 반응하지 못하고 계속 대기하게 될 것이다. 따라서 사용자 경험의 품질이 매우 나빠질 수 있다. 이런 경우에는 “동시작업 처리”를 하도록 해주는 것이 좋다. 동시작업 처리를 사용하면 멀티 코어 프로세서를 효율적으로 사용할 수 있고, 시스템을 보다 “바삐” 움직이게 할 수 있기 때문이다.

(*여기서 주목해야 할 부분은 “동시작업 처리”를 “멀티 스레드”로 기재하지 않은 것이다. GCD에서 동시작업을 처리하는 것은 “디스패치 큐”를 분리하여 동시에 2개 이상의 작업을 진행시키는 것인데, 놀라운 점은 GCD를 사용한 동시작업은 해당 작업에서 다시 스레드를 생성하지 않는 이상, 모두 메인 스레드에서 돌아간다. 따라서 멀티스레드가 아닌 경우가 있을 수 있다.)

NSOperationQueue를 통한 동시작업

먼저 NSOperationQueue를 사용하는 경우를 살펴보도록 하자.

만약 Block 객체를 사용하는 코딩 문법에 조금 익숙하다면 (특히 이는 애니메이션과 관련한 새로운 메소드에서 자주 등장한다. 우리가 익힌 바 있는 UIDocument 관련 글에서도 본 적이 있을 것이다.) 상당히 쉽게 익숙해질 수 있다. 즉 NSOperation은 코드 블럭과 같이 “일련의 작업을 지시하는 코드”를 객체로 만들어 이를 별도의 큐에서 실행하도록 하는 방식이다. 이 때 스레드의 생성과 관리는 큐가 알아서 하게 되므로 여전히 스레드 관리에 대한 크나큰 부담을 덜 수 있게 되는 것이다.

작업 객체 생성

NSOperation은 우리가 작업해야 하는 코드를 담는 객체인데, 이를 활용하는 방법에는 다음 세 가지가 있다.

  • NSInvocationOperation
  • NSBlockOperation
  • subclassing NSOperation
먼저 NSInvocation은 특정 객체의 메소드를 작업 객체로 만들어버리는 방법이다. 즉, 다른 스레드에서 동시 처리를 해야할 메소드를 가진 객체가 있다면, 그 객체의 메소드를 동시 처리 작업으로 만들 수 있다.
NSInvocationOperation *theOp = [[NSInvocationOperation alloc] 
                                      initWithTarget:self 
                                            selector:@selector(doMyTask:) 
                                              object:withData];

NSBlockOperation 객체는 코드 블럭을 사용해서 작업 객체를 만들 수 있다.

NSBlockOperation *theBlockOp = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"Block has started");
}];

// 아래와 같이 블럭을 계속 추가해 나갈 수 있음
[theBlockOp addBlock:^{
    // do something...
}];

혹은 NSOperation 객체를 새로 생성할 수도 있다. (이에 대한 자세한 내용은 다른 글에서 다뤄볼까 한다.)

작업 객체의 실행

작업 객체는 물론 그대로도 실행이 가능하다. 하지만 특별히 멀티 스레드로 동작하도록 작업 객체를 커스터마이징 하지 않은 경우라면 이런 작업들은 메인 스레드에서 돌아가는 함수와 동일하다. (즉, 동시작업으로 처리되지 않는다.) 별도의 스레드에서 동시 작업으로 처리되도록 하려면 NSOperationQueue 객체를 생성하여, 이 곳에 앞서 말한 방법으로 생성된 작업객체를 추가해주면 된다.

NSOperationQueue *aQueue = [[NSOperationQueue alloc] init];
[aQueue addOperation:anOp];

작업이 추가되면 큐 객체는 자동으로 스레드를 만들고 먼저 큐에 추가된 순서대로 작업 객체에 start 메시지를 보내어 각각의 작업을 시작하게 된다.

단순한 예제를 만들 때 유의할 점은, 큐가 스레드를 새로 생성할 때는 약간의 시간이 걸리는 데, 그 사이에 메인 스레드가 종료되어 버린다면 큐에 담긴 작업이 아예 처리되지 못하고 프로그램이 종료될 수도 있다.

이런 예제와 같은 경우에는 큐를 처리하는 동안 큐를 생성했던 현재 스레드를 잠깐 멈추게 하여 큐가 처리된 이후에 그 다음 작업을 실행해주는 방법도 있다.

[aQueue waitUntilAllOperationsAreFinished];

하지만 이렇게 큐의 작업이 처리되는 것을 기다리는 것은 성능에도 좋지 않은 영향을 미치고 (왜냐면 그만큼 메인 스레드가 블럭킹을 당하고 잠기기 때문에) 되려 동시 작업성을 저해하는 결과를 가져오기 때문에 가능하면 쓰지 말 것을 권한다.

큐에서 작업을 시작할 때는 작업 객체에 start 메시지를 보낸다. 이와 같은 방법으로 NSInvocationOperation 객체나 NSBlockOperation 객체에 start 메시지를 보내 해당 작업을 실행시킬 수 있다. 하지만 메인 스레드에서 명시적으로 이런 작업을 실행하는 것은 그냥 코드 블럭을 실행하는 것과 아무런 차이가 없게된다.

Dispatch Queue 사용하기

Dispatch Queue도 큐에 코드 블럭을 밀어넣어 실행하는 것과 유사하게 디스패치 큐에 작업(코드 블럭)을 넣고 이를 동시에 실행시키는 방법이다. 동시 작업으로 진행될 작업은 메인 스레드에서 함께 돌아간다. 이것이 오퍼레이션 큐와의 가장 큰 차이점이라 하겠다. (실은 항상 메인 스레드에서 돌아가는지는 모르겠다. 동시에 처리되는 작업의 개수도 시스템이 코어의 개수나 시스템에 현재 걸려 있는 부하에 따라 자동으로 판별한다.

즉 동시 작업으로 병렬처리되는 일이 종료되었을 때 어떤 일이 이어서 일어나게 만들고자 할 때 (이때는 델리게이션이나 KVO를 써도 되지만) 이 방법을 사용하는 것도 굉장히 쉽고 간단하다. GCD를 이용해서 병렬작업을 처리하는 가장 간단한 방법은 글로벌 큐를 사용하는 것이다. 물론 글로벌 큐를 사용하지 않고 별도의 큐를 생성하여 작업을 처리할 수도 있다. 단 이렇게 생성되는 큐는 serial 큐로, 추가된 순서대로 작업이 수행된다. 대신 글로벌 큐는 들어간 순서대로 작업이 시작되나, 큐에 들어간 작업은 가능한 많은 수가 동시에 실행되므로 먼저 들어간 작업이 먼저 끝난다고는 특정할 수 없다.

큐에 작업을 추가하여 실행하기 위해서는 dispatch_async 함수를 사용한다. 이 함수에 수행할 작업을 코드 블럭으로 넘겨서 수행하도록 할 수 있다. 이 함수를 호출한 직후 프로그램의 흐름은 다음 라인으로 넘어가고, 디스패치 큐는 이와 동시에 넘겨진 작업을 즉시 처리하게 된다. 다음과 같이 글로벌 큐를 적용한 아주 간단한 코드를 사용해서 병렬 작업을 수행할 수 있다.

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
        // 병렬적으로 진행할 코드
        });

매우 큰 DB에서 값을 검색해 오거나, 인터넷을 통해 데이터를 다운로드 받아서 처리해야 하거나, 영구저장소에 저장된 파일을 액세스 하는 등 시간이 걸릴 수 있는 일을 처리하는 경우에는 메인스레드가 blocking 될 수 있으므로 이렇게 처리해주면 백그라운드에서 돌아가는 것처럼 처리되고 UI 반응은 멈추지 않고 계속 이루어질 수 있다.

만약 저렇게 큐에서 돌아가는 작업이 끝나거나 혹은 그 중간에 UI를 업데이트 하거나 해야 한다면, UI 갱신을 처리하는 부분은 메인 큐이므로 메인 큐에서 필요한 작업을 처리할 수 있다. 즉 메인 큐 ▶ 글로벌 큐에서 동시작업 ▶ 메인큐에서 작업 하는 식으로 중간에 메인 큐에 끼어들 수도 있다.

dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^{
        // 병렬적으로 진행할 코드
        dispatch_async(dispatch_get_main_queue(), ^{
            //메인큐에서 UI 업데이트 등을 실행
            });
        });

참고자료 : Concurrency Programming Guide