[Objective-C 문법] GCD를 사용한 Concurrent Programming

개요

Concurrent Programming은 iOS 앱뿐만이 아니라 계산을 처리하는 CPU 코어가 하나 이상 사용되어지는 곳에서는 보다 효율적인 코딩을 위해서 사용되어 지는 기법이다. 사실 CPU는 한번에 한 가지의 계산 처리밖에 할 수 없는 존재이다. 하지만 워낙에 빠르기 때문에 시간을 쪼개어서 여러가지 일을 돌아가면서 실행하기 때문에 엔드 유저들에게 있어서 한번에 여러가지 일이 동시에 실행되어지는 것으로 착각하게 만든다. 하지만 하드웨어의 발전을 통해서 계산 처리 속도도 빠르게 업그레이드 했지만, 하드웨어 발전의 한계에 도달한 나머지 이제는 한 개의 코어 자체의 처리 속도를 올리려고 하기 보다는, 여러가지 처리르 동시에 할 수 있도록 여러 개의 코어를 한 CPU 칩에 탑재하는 방식의 최신 기술이 나오고 있는 실정이다.

하지만 이러한 혜택을 누리기 위해서는 하드웨어 뿐만이 아니라 소프트웨어 차원에 기법이 적용되었을 때 비로써 진정한 Concurrent 프로그래밍의 힘을 발휘할 수 있게 되는 것이다.

그렇다면 애플의 Objective-C를 사용한 프로젝트에서는 어떤 기법을 통해 Concurrent 프로그래밍을 할 수 있는지 알아보자. 먼저 애플은 다음 세가지 기법을 사용하여 Concurrent 프로그래밍을 할 수 있을 것이다.

  • GCD (Grand Central Dispatch)를 사용한 기법.
  • NSOperation이 포함되어 있는 Foundation 프래임워크를 사용한 기법.
  • NSThread 직접 쓰레드를 사용한 기법.

이번 포스팅에서는 GCD에만 집중하여 설명을 하게 될 것이다. 위의 기법이 어떻게 다른지 서로 비교하기 위해서는 먼저 하나하나 사용할 문법에 대해서 알아보는 것이 필요할 것이다.

1. 메인 큐에서 UI 관련 된 Task 실행하기

아이폰은 안드로이드 폰에 비해서 원활한 UI이 작동이 큰 장점 중 하나이다. 그것이 가능한 이유는 모든 UI 관련 처리는 main_queue에서만 처리를 하기 때문에 어떤 로직이 오래 동안 계산한다고 해서 UI 작동을 막지 못하기 때문이다. 그렇다면 어떻게해서 UI 관련 작업을 GCD를 통해 처리 할 수 있는지 아래 코드를 살펴보자.

  1. dispatch_queue_t mainQueue = dispatch_get_main_queue();
  2. dispatch_async(mainQueue, ^
  3. {
  4.     UIAlertView *alert = nil;
  5.     alert = [[UIAlertView alloc] initWithTitle:@"GCD TEST" 
  6.                                  message:@"HelloBarty" 
  7.                                  delegate:nil 
  8.                                  cancelButtonTitle:@"OK" 
  9.                                  otherButtonTitles:nil, nil];
  10.  
  11.     [alert show];
  12. });

 

2. UI 처리와 관련 없는 작업을 Synch하게 처리하기

큐에 들어 온 순서대로 처리하고 싶다면, 다음 코드를 참조하여 처리한다. 그런데 기억해야 할 것은 UI 작업이 처리될 main 큐에서는 순차적인 처리를 안해준다. (애플에서는 화면을 멈추게 하는 것을 막기위한 처신으로 보여진다.)

  1. void (^Print1To1000)(void) = ^
  2. {
  3.     NSUInteger counter = 0;
  4.     for(counter = 1; counter <= 1000; counter++)
  5.     {
  6.         NSLog(@"counter = %lu – Thread = %@", counter, [NSThread currentThread]);
  7.     }
  8. };
  9.  
  10. void (^Print1To100)(void) = ^
  11. {
  12.     NSUInteger counter = 0;
  13.     for(counter = 1; counter <= 100; counter++)
  14.     {
  15.         NSLog(@"Counter = %lu – Thread = %@", counter, [NSThread currentThread]);
  16.     }
  17. };
  18.  
  19. - (void)executeSynchrously
  20. {
  21.     dispatch_queue_t concurrentQue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  22.  
  23.     dispatch_sync(concurrentQue, Print1To1000);
  24.     dispatch_sync(concurrentQue, Print1To1000);
  25. }

 

3. UI 처리와 관련 없는 작업을 Asynch하게 처리하기

이번에는 순차적인 처리가 아닌 비동기적인 처리하는 법을 알아 보겠다. 크게 두가지 코드를 통해 알아 볼 것인다. 첫 번째 코드는 다음과 같다.

  1. void (^Print1To1000)(void) = ^
  2. {
  3.     NSUInteger counter = 0;
  4.     for(counter = 1; counter <= 1000; counter++)
  5.     {
  6.         NSLog(@"counter = %lu – Thread = %@", counter, [NSThread currentThread]);
  7.     }
  8. };
  9.  
  10. void (^Print1To100)(void) = ^
  11. {
  12.     NSUInteger counter = 0;
  13.     for(counter = 1; counter <= 100; counter++)
  14.     {
  15.         NSLog(@"Counter = %lu – Thread = %@", counter, [NSThread currentThread]);
  16.     }
  17. };
  18.  
  19. - (void)executeAsynchrously
  20. {
  21.     dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  22.  
  23.     dispatch_async(concurrentQueue, Print1To100);
  24.     dispatch_async(concurrentQueue, Print1To100);
  25. }

이번에는 네트워크를 통해 이미지 데이터를 다운로드 받아온 다음 ViewController에 집어 넣는 작업을 해보자.

  1. - (void)downloadImgAndShowAsynchronously
  2. {
  3.     dispatch_queue_t mainThread = dispatch_get_main_queue();
  4.     dispatch_queue_t concurrentQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  5.  
  6.     dispatch_async(concurrentQ, ^
  7.     {
  8.         __block UIImage *image = nil;
  9.  
  10.         //#. Download Task
  11.         dispatch_sync(concurrentQ, ^
  12.           {
  13.               NSLog(@"## 1");
  14.  
  15.               NSString *urlStr = @"http://yannickloriot.com/wp-content/uploads/2011/04/xcode.png";
  16.               NSURLRequest *req = [NSURLRequest requestWithURL:[NSURL URLWithString:urlStr]];
  17.               NSError *downloadError = nil;
  18.               NSData *imageData = [NSURLConnection
  19.                                    sendSynchronousRequest:req
  20.                                    returningResponse:nil
  21.                                    error:&downloadError];
  22.               if (downloadError == nil && imageData != nil)
  23.                   image = [UIImage imageWithData:imageData];
  24.               else if(downloadError != nil)
  25.                   NSLog(@"Error: %@", downloadError);
  26.               else
  27.                   NSLog(@"No data could get downloaded from URL..");
  28.           });
  29.  
  30.         //#. Show image Task
  31.         dispatch_sync(mainThread, ^
  32.           {
  33.               NSLog(@"## 2");
  34.  
  35.               if (image == nil)
  36.               {
  37.                   NSLog(@"Image isn't downloaded. Nothing to display.");
  38.                   return;
  39.               }
  40.  
  41.               UIImageView *imageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
  42.               [imageView setImage:image];
  43.               [imageView setContentMode:UIViewContentModeScaleAspectFit];
  44.               [self.view addSubview:imageView];
  45.           });
  46.     });
  47. }

위 예제 코드를 보면 큰 Asynch 괄호에 두 개의 작업이 synch로 선언되어 있다. 이렇게 작업한 이유는 이미지 데이터가 다운로드가 끝난 다음에 ‘main 큐’에서 UI로 뿌려주기 위함이다. 하지만 본 블러그에서 잠시 언급했었지만, 원래 main 큐에서는 동기식 처리가 안되나, 이 작업은 비동기식 틀안에 있는 동기식 작업이기 때문에 처리가 허용된다. 만약 main 큐에다가 동기식 처리를 요청하게 되면은 Runtime 때 실행 조차 되지 않는다.

 

4. 작업을 지연하여 실행하기

GCD를 사용하여 작업에 타이머를 줄 수도 있다. 자동 Xcode 5의 자동완성 기능이 ‘dispatch_after’만 쳐도 코드를 완성해주는데 다음과 같다.

  1. double delayInSeconds = 5.0;
  2. dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
  3. dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  4.  
  5. dispatch_after(popTime, concurrentQueue, ^(void)
  6. {
  7.     NSLog(@"Hello Barty!");
  8. });

 

5. 애플리케이션이 살아 있는 동안 한번만 실행하기

디자인 패턴에서는 ‘싱글톤 패턴’이 해당 기능의 역할을 한다. 앱이 살아 있는 동안 한번만 실행되게 해주는 기법인다. GCD를 통해서도 실현할 수 있다.

  1. void (^performThisOnlyOneTime)(void) = ^
  2. {
  3.     static NSUInteger numberOfEntries = 0;
  4.     numberOfEntries ++;
  5.  
  6.     NSLog(@"Executed %lu time(s)", (unsigned long)numberOfEntries);
  7. };
  8.  
  9. - (void)performOnce
  10. {
  11.     static dispatch_once_t onceToken;
  12.  
  13.     dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
  14.     dispatch_once(&onceToken, ^
  15.     {
  16.         dispatch_async(concurrentQueue, performThisOnlyOneTime);
  17.     });
  18.  
  19.     return;
  20. }

그래서 실제로 싱글톤 패턴을 구현할 때 GCD를 사용하여 구현하는 경우도 많이 있다.

 

6. 그룹화 된 작업 처리하기

그룹화 된 작업을 처리하는 GCD가 있다.

  1. - (void)reloadTableView
  2. {
  3.     NSLog(@"reloadTableView");
  4. }
  5.  
  6. - (void)reloadScrollView
  7. {
  8.     NSLog(@"reloadScrollView");
  9. }
  10.  
  11. - (void)reloadImageView
  12. {
  13.     NSLog(@"reloadImageView");
  14. }
  15.  
  16. - (void)performGropingTasks
  17. {
  18.     dispatch_group_t group = dispatch_group_create();
  19.     dispatch_queue_t mainQueue = dispatch_get_main_queue();
  20.  
  21.     dispatch_group_async(group, mainQueue, ^
  22.     {
  23.         [self reloadTableView];
  24.     });
  25.  
  26.     dispatch_group_async(group, mainQueue, ^
  27.     {
  28.         [self reloadScrollView];
  29.     });
  30.  
  31.     dispatch_group_async(group, mainQueue, ^
  32.     {
  33.         [self reloadImageView];
  34.     });
  35.  
  36.     dispatch_group_notify(group, mainQueue, ^
  37.     {
  38.         UIAlertView *alertView = nil;
  39.         alertView = [[UIAlertView alloc] initWithTitle:@"Finished"
  40.                                                message:@"All tasks are finished"
  41.                                               delegate:nil
  42.                                      cancelButtonTitle:@"OK"
  43.                                      otherButtonTitles:nil, nil];
  44.  
  45.         [alertView show];
  46.     });
  47. }

 

7. 사용자 정의 큐 만들어 사용하기

사용자가 이름을 주고 순차적인 큐를 만들 수 있다. 보통 이름을 줄때는 ‘reversed domain 방식’으로 메겨진다 다음 코드르 살펴보자.

  1. - (void)performTasksOnSerialCustomeQueue
  2. {
  3.     dispatch_queue_t customQue = dispatch_queue_create("com.barty.personal.gcd", 0);
  4.  
  5.     dispatch_async(customQue, ^
  6.     {
  7.         for (int i = 0; i < 5; i++)
  8.         {
  9.             NSLog(@"#1. iteration: %d", i);
  10.         }
  11.     });
  12.  
  13.     dispatch_async(customQue, ^
  14.    {
  15.        for (int i = 0; i < 10; i++)
  16.        {
  17.            NSLog(@"#2. iteration: %d", i);
  18.        }
  19.    });
  20.  
  21.     dispatch_async(customQue, ^
  22.    {
  23.        for (int i = 0; i < 15; i++)
  24.        {
  25.            NSLog(@"#3. iteration: %d", i);
  26.        }
  27.    });
  28. }

 
복잡한 Thread 처리 없이 여러가지 기능을 제공해주는 GCD는 요즘 한참 유행처럼 많은 개발자들 사이에서 사용되어 지고 있다. 하지만 간편한 사용에 비해 ‘취소하기’와 같은 기능이 없어서 아쉬울 때가 있다. 무겁지 않은 가벼운 concurrent를 처리할 때에는 GCD만큼 간변하고 좋은 것이 없는 것 같다. 하지만 GCD가 채워주지 못하는 부분은 NSOperation이나 NSThread에서 처리를 해보자.

[링크] How to add a new Unit Test target and OCMock to an existing XCode project

평소에 TDD에 원낙 관심이 많은 나는 어떻게하면 iOS 개발에 있어서 커버리지 100%를 자랑하면 회사 동료들에게 말할 수 있을까 고민 중이었다. 그런데 항상 기본 XCode에서 제공해주는 기능을 가지고 테스트하는 부분에 있어서 항상 한계를 느꼈는데, OCMock이라는 라이브러리를 발견하고 Cocopod 라이브러리를 써서 적용하려고 고생을 하다가. 좋은 글을 하나 찾아서 링크를 걸 어보았다.

링크