[iOS] 런타임에 언어 바꾸기

iOS 프로젝트에서 Localizable.string 파일을 만들어 앱의 다양한 언어를 지원할 수 있다. 기본적으로 유저가 사용하고 있는 OS 세팅에 적용되어 있는 언어를 앱은 받아드리고 보여주게 될 것이다. 하지만 필자 같이 영문 OS 버전을 사용하면서 특정 앱에 한에서 한국어를 사용하고 싶을 때가 있다. 하지만 CocoaTouch에 선언되어 있는 NSLocalizedString 매크로를 통해서 앱의 언어를 바꾸려면 앱을 메모리에서 한번 지우고 다시 앱을 열어야지만 새 언어를 적용할 수 있다는 특징이 있다. 꽨 번거로운 일이다. 그래서 필자는 런타임에 언어를 바꿀수 있는 방법을 찾아보다가 괜찮은 방법을 찾았기에 소개하고자 한다.

본 글은 stackoverflow에서 찾은 솔루션이다.

링크

기본적인 아이디어는 애플에서 적용되어 있는 NSLocalizedString 매크로를 재정의 하여 사용하는 것이다.

  1. NSBundle의 카테고리를 만든다.
  2. AppDelegate에 language 코드를 알려주는 인스턴스 메서드를 만든다.
  3. NSLocalizedString 매크로를 사용하는 파일에 카테코리 파일을 임포트 한다.

NSBundle의 카테고리 만들기

  1. //
  2. //  NSBundle+RunTimeLanguage.h
  3. //  Copyright © 2016 bartysways. All rights reserved.
  4. //
  5.  
  6. @import Foundation;
  7.  
  8. #undef NSLocalizedString
  9. #define NSLocalizedString(key, comment) [[NSBundle mainBundle] runTimeLocalizedStringForKey:(key) value:@"" table:nil]
  10.  
  11. @interface NSBundle (RunTimeLanguage)
  12.  
  13. - (NSString *)runTimeLocalizedStringForKey:(NSString *)key
  14.                                      value:(NSString *)value
  15.                                      table:(NSString *)tableName;
  16.  
  17. @end
  18.  
  19. //
  20. //  NSBundle+RunTimeLanguage.m
  21. //  Copyright © 2016 bartysways. All rights reserved.
  22. //
  23.  
  24. #import "NSBundle+RunTimeLanguage.h"
  25. #import "AppDelegate.h"
  26.  
  27. @implementation NSBundle (RunTimeLanguage)
  28.  
  29. - (NSString *)runTimeLocalizedStringForKey:(NSString *)key
  30.                                      value:(NSString *)value
  31.                                      table:(NSString *)tableName
  32. {
  33.     AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
  34.     NSString *path = [[NSBundle mainBundle] pathForResource:appDelegate.languageCode
  35.                                                      ofType:@"lproj"];
  36.     NSBundle *languageBundle = [NSBundle bundleWithPath:path];
  37.     NSString *localizedString= [languageBundle localizedStringForKey:key value:key table:nil];
  38.  
  39.     return localizedString;
  40. }
  41.  
  42. @end

AppDelegate에 인스턴스 메서드 만들기

  1. //
  2. //  AppDelegate.h
  3. //  Copyright © 2015 bartysways. All rights reserved.
  4. //
  5.  
  6. @import UIKit;
  7.  
  8. @interface AppDelegate : UIResponder
  9. <
  10. UIApplicationDelegate
  11. >
  12.  
  13. @property(nonatomic, strong) UIWindow *window;
  14. @property(nonatomic, strong, readonly) NSString *languageCode;
  15.  
  16. @end
  17.  
  18. //
  19. //  AppDelegate.m
  20. //  Copyright © 2015 bartysways. All rights reserved.
  21. //
  22.  
  23. @import UserNotifications;
  24.  
  25. #import "AppDelegate.h"
  26.  
  27. @implementation AppDelegate
  28.  
  29. - (NSString *)languageCode
  30. {
  31.     NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
  32.     NSString *languageCode = ((NSArray *)[userDefaults objectForKey:@"AppleLanguages"]).firstObject;
  33.     return languageCode != nil ? languageCode : @"en";
  34. }
  35. @end

카테코리 파일을 임포트하기

  1. //
  2. //  HeaderCollectionReusableView.m
  3. //  Copyright © 2016 bartysways. All rights reserved.
  4. //
  5.  
  6. #import "HeaderCollectionReusableView.h"
  7. #import "NSBundle+RunTimeLanguage.h"
  8.  
  9. @implementation HeaderCollectionReusableView
  10. @end

[번역글] Concurrent Programming: APIs and Challenges

This posting is a Korean translation of “Objc.io”‘s contents. Please contact me if I need to remove it. Thank you.

Original article: Concurrent Programming: APIs and Challenges

 

동시성(Concurrency)라는 개념은 여러 과업(tasks)이 같은 시간에 처리되는 것을 뜻한다. 이것은 싱글 CPU 코어 환경에서는 시간 분할 처리 방식이나, 멀티 CPU 코어 환경에서 진정한 병렬 처리 방식과 같이 구현될 수 있다.

OSX와 iOS는 병행 프로그래밍(Concurrent programming)을 가능케 하는 다양한 API를 제공한다. 각각 API는 다 다른 능력과 한계를 가지고 있으며, 여러 종류의 과업에 알맞게 설계되었다. 그리고 각 API는 다 다른 추상화(Abstraction) 정도의 레벨로 구현되어 있다. 하위 레벨 정도의 조작이 가능하지만 그에 따른 책임도 따른다.

병렬 프로그래밍은 난해한 문제들과 상황들이 존재하는 매우 까다로운 주제이며, GCD나 NSOperationQueue와 같은 API를 사용하면서 쉽게 까먹을 수 있는 개념이다. 먼저 이 글에서는 OSX와 iOS의 병렬처리 관련된 API를 전체적으로 살펴 볼 것이며, 전부터 내려오던 병렬 프로그래밍의 어려운 과제들에 대해서 깊이 알아볼 것이다.

 

OS X와 iOS의 병렬 API

애플의 모바일과 데스크톱 OS는 병렬 프로그래밍을 위한 동일한 API를 제공한다. 이 글에서는 pthread와 NSThread, GCD, NSOperationQueue, 그리고 NSRunLoop을 살펴 볼 것이다. 기술적으로 run loop이 소개 된다는 것이 이상할지 모른다. 왜냐하면 진정한 병렬처리와 관계가 없기 때문이다. 하지만 이 주제와 함께 자세히 알아볼 필요가 있다고 생각한다.

우리는 먼저 하위 레벨 API를 시작으로 점점 상위 레벨 API를 살펴 볼 것이다. 이렇게 작성한 이유는 상위 API들은 하위 API들 위에 만들어진 것이기 때문이다. 아무튼 실제 개발에서는 지금과 같은 방식이 아닌 상위 API를 먼저 검토하고 사용해야 할 것이다. 왜냐하면 코드를 쉽게 만들고 병렬 처리 모델을 더 심플하게 유지 할 수 있기 때문이다.

만약 우리가 왜 상위 레벨 추상화와 매우 심플한 병렬 처리 코드를 계속적으로 추천하는지 궁금 할 것이다. 그렇다면 이글의 두번째 부분과 Peter Steinberger가 작성한 쓰레드 세이프관련 포스트를 읽어보길 추천한다.

 

쓰레드

쓰레드는 운영체재의 스케쥴러에 의해서 독립적으로 스케쥴이 잡히는 프로세스의 하위 유닛이다. 가상으로 GCD와 NSOperationQueue의 모든 병렬 처리 API는 쓰레드 위해서 만들어 졌다.

하나 이상의 쓰레드는 싱글 CPU 코어에서도 실행될 수 있다. (최소한 느끼기에 동시에 실행되고 있다고 느끼게 해준다.) 운영체제는 각 쓰레드에게 짧은 시간을 할당해 주며 계산을 하는데, 사용자는 여러 쓰레드가 동시에 실행 되는 처럼 느낄 것이다. 만약 하나 이상의 CPU 코어가 탑재 되어 있다면 실제로 병렬 처리를 하여 특정 과업의 전체 시간을 줄여 줄 것이다.

멀티 CPU 코어에서 당신의 코드와 당신이 사용하고 있는 프레임워크 코드가 실행 스케쥴을 얼마나 잡는지 확인하고 싶다면, XCode의 Instruments의 CPU strategy를 사용하된다.

마음에 염두하고 있어야 할 것은 우리에게는 언제 어디서 작성한 코드가 스케쥴 잡힐지 모른다는 것이다. 그리고 또한 언제 얼마나 길게 할당을 위해 기달려야 자신의 차례가 오는지도 알 수 없다. 이런 종류의 쓰레드 스케쥴링은 매우 강력한 테크닉이다. 하지만 매우 난이도 높은 작업이다. 우리는 앞으로 이것을 조사해 볼 것이다.

복잡도에 대한 내용은 잠시 뒤로하고, POSIX 쓰레드 API를 사용하든지 Objective-C로 랩핑되어 있는 NSThread를 사용하여 자신만의 쓰레드를 만들수 있다. 다음은 pthread를 사용하여, 100만의 숫자 가운데서 가장 작은 수와 가장 큰수를 찾는 간단한 샘플 코드이다. 4개의 쓰레드가 병렬 처리하면서 찾을 것이다. 코드를 살펴 보면 왜 pthread를 직접 쓰지 말아야 할 지 알게 될 것이다.

 

  1. #import <pthread.h>
  2.  
  3. struct threadInfo {
  4.     uint32_t * inputValues;
  5.     size_t count;
  6. };
  7.  
  8. struct threadResult {
  9.     uint32_t min;
  10.     uint32_t max;
  11. };
  12.  
  13. void * findMinAndMax(void *arg)
  14. {
  15.     struct threadInfo const * const info = (struct threadInfo *) arg;
  16.     uint32_t min = UINT32_MAX;
  17.     uint32_t max = 0;
  18.     for (size_t i = 0; i < info->count; ++i) {
  19.         uint32_t v = info->inputValues[i];
  20.         min = MIN(min, v);
  21.         max = MAX(max, v);
  22.     }
  23.     free(arg);
  24.     struct threadResult * const result = (struct threadResult *) malloc(sizeof(*result));
  25.     result->min = min;
  26.     result->max = max;
  27.     return result;
  28. }
  29.  
  30. int main(int argc, const char * argv[])
  31. {
  32.     size_t const count = 1000000;
  33.     uint32_t inputValues[count];
  34.  
  35.     // Fill input values with random numbers:
  36.     for (size_t i = 0; i < count; ++i) {
  37.         inputValues[i] = arc4random();
  38.     }
  39.  
  40.     // Spawn 4 threads to find the minimum and maximum:
  41.     size_t const threadCount = 4;
  42.     pthread_t tid[threadCount];
  43.     for (size_t i = 0; i < threadCount; ++i) {
  44.         struct threadInfo * const info = (struct threadInfo *) malloc(sizeof(*info));
  45.         size_t offset = (count / threadCount) * i;
  46.         info->inputValues = inputValues + offset;
  47.         info->count = MIN(count – offset, count / threadCount);
  48.         int err = pthread_create(tid + i, NULL, &findMinAndMax, info);
  49.         NSCAssert(err == 0, @"pthread_create() failed: %d", err);
  50.     }
  51.     // Wait for the threads to exit:
  52.     struct threadResult * results[threadCount];
  53.     for (size_t i = 0; i < threadCount; ++i) {
  54.         int err = pthread_join(tid[i], (void **) &(results[i]));
  55.         NSCAssert(err == 0, @"pthread_join() failed: %d", err);
  56.     }
  57.     // Find the min and max:
  58.     uint32_t min = UINT32_MAX;
  59.     uint32_t max = 0;
  60.     for (size_t i = 0; i < threadCount; ++i) {
  61.         min = MIN(min, results[i]->min);
  62.         max = MAX(max, results[i]->max);
  63.         free(results[i]);
  64.         results[i] = NULL;
  65.     }
  66.  
  67.     NSLog(@"min = %u", min);
  68.     NSLog(@"max = %u", max);
  69.     return 0;
  70. }

 

NSThread는 pthread를 랩핑한 Objective-C 클래스이다. 이렇게 랩핑을 한 것이 코코아 환경에서 더 익숙해 보인다. 예를 들어, 백그라운드에서 은닉화하고 싶은 코드를 쓰레드로 작성하고 싶다면, NSThread를 상속 받아서 작성하면 된다.

&nbsp

  1. @interface FindMinMaxThread : NSThread
  2. @property (nonatomic) NSUInteger min;
  3. @property (nonatomic) NSUInteger max;
  4. - (instancetype)initWithNumbers:(NSArray *)numbers;
  5. @end
  6.  
  7. @implementation FindMinMaxThread {
  8.     NSArray *_numbers;
  9. }
  10.  
  11. - (instancetype)initWithNumbers:(NSArray *)numbers
  12. {
  13.     self = [super init];
  14.     if (self) {
  15.         _numbers = numbers;
  16.     }
  17.     return self;
  18. }
  19.  
  20. - (void)main
  21. {
  22.     NSUInteger min;
  23.     NSUInteger max;
  24.     // process the data
  25.     self.min = min;
  26.     self.max = max;
  27. }
  28. @end

 

새로운 쓰레드를 시작하려면 쓰레드 객체를 만들어서 다음과 같이 start 메서드를 호출해야 한다.

  1. NSMutableSet *threads = [NSMutableSet set];
  2. NSUInteger numberCount = self.numbers.count;
  3. NSUInteger threadCount = 4;
  4. for (NSUInteger i = 0; i < threadCount; i++) {
  5.     NSUInteger offset = (numberCount / threadCount) * i;
  6.     NSUInteger count = MIN(numberCount – offset, numberCount / threadCount);
  7.     NSRange range = NSMakeRange(offset, count);
  8.     NSArray *subset = [self.numbers subarrayWithRange:range];
  9.     FindMinMaxThread *thread = [[FindMinMaxThread alloc] initWithNumbers:subset];
  10.     [threads addObject:thread];
  11.     [thread start];
  12. }

 

먼저 결과를 평가하기 전에 우리가 새로 작성한 쓰레드의 모든 코드가 끝나고 나서 thread 객체의 isFinished 프로퍼티를 알고 싶다면, 관심이 있는 독자들은 한번 직접 짜보길 권한다. 여기에서 진짜 말하고 싶었던 부분은 쓰레드를 직접 사용해 본다는 부분이다. pthread를 사용하든지, NSThread의 API를 사용하는 것은 우리 멘탈 모델에 적합하지 않는 비교적 시대에 뒤떨어지는 경험일지 모르지만 말이다.

마음에 직접적으로 떠오르는 한 가지 문제는, 만약 당신의 코드나 프레임워크 코드가 자신들의 쓰레드를 만들어 작동 한다면, 활성화 되는 쓰레드의 숫자가 기하급수적으로 늘어난다는 것이다. 해당 이슈는 큰 프로젝트에서 자주 발생하는 문제이다. 예들 들어, CPU 코어가 8개가 있는 환경에서 이점을 챙기기 위해 쓰레드 8개를 생성해서 사용한다고 치자, 각 쓰레드에서 위 로직이 들어 있는 프레임워크를 사용하게 되면, 결국 짧은 시간내에 수 백개나 되는 쓰레드를 만들어 낼 것이다. 각 코드들은 자신들의 책임을 다하여 작동할 것인데, 그럼에도 불구하고 결과는 좋은 모습이 아니다. 쓰레드를 잘 사용하기는 쉽지 않다. 각 쓰레드는 메모리와 커널 자원과 연결 되어 있기 때문이다.

다음으로, 우리는 큐 기반의 병령처리 관련 API를 살펴 볼 것이다. GCD와 NSOperationQueue인데, 이것들은 집중적으로 쓰레드 풀를 관리하여 해당 문제점들을 완화해 준다.

Grand Central Dispatch

늘어나는 사용자 디바이스의 CPU 코어 갯수의 이점을 개발자들이 누릴 수 있도록 애플은 Grand Central Dispatch를 OSX 10.6과 iOS4에서 부터 제공하기 시작했다. 하위 레벨의 병렬처리 API인 GCD에 대해서 이 글에서 더 자세히 알아 볼 것이다.

GCD를 사용하면 이제 더 이상 쓰레드를 직접적으로 상대하지 않아도 된다. 대신에 코드 블럭을 큐에 넣으면 밑 물에서 GCD가 쓰레드 풀을 관리 한다. GCD는 어떤 특정 코드 블럭의 쓰레드를 실행할지 정하고, 시스템의 가용 리소스에 따라서 쓰레드들을 관리한다. 쓰레드는 중앙 통제적으로 관리 되어지기 때문에 쓰레드 때문에 생기는 많은 문제점들을 줄어들게 된다.

또 다른 이점으로는 어플리케이션 개발자 입장에서는 복잡한 쓰레드 개념보다 큐 개념을 가지고 개발할 수 있게 되었다는 점이다. 새로운 병렬처리 개념은 기존 방식에 비해 쉽게 작업할 수 있다.

GCD는 다섯 종류의 큐를 제공한다. 메인 쓰레드위에서 도는 메인 큐, 세 개의 우선순위가 다른 백그라운드 큐, 그리고 I/O 처리같은 작업을 할 수 있는 보다 더 낮은 우선순위를 가지고 있는 백그라운드 큐이다. 더 나아가, 순차적 혹은 비동기식 커스텀 큐를 만들 수도 있다. 이렇게 강력한 추상화를 지닌 커스텀 큐는 특정한 하나의 글로벌 큐에 끼어 들어가게 되어 해당 쓰레드 풀에서 연관되게 된다.

Thread Pool

처음 다른 우선순위로 여러가지 큐를 만들어 사용한다는 것은 직관적으로 보일지 모른다. 하지만 우리는 어떤 경우에든지 여러분이 디폴트 우선순위가 지정되어 있는 큐를 최대한 사용하라고 권하고 싶다. 공유자원을 접근하는 과업을 작업하다 보면, 실행고자 하는 과업을 각 다른 우선순위와 함께 큐에 스케쥴링한다는 것이 예상하지 못한 작동을 불러이르킨다는 것을 알게 될 것이다. 또한 낮은 우선순위 과업이 높은 우선순위 과업을 가로막는 일이 잦아져, 프로그램 전체가 점점 느려지다가 멈추게 만들기도 한다. 이런 현상을 우선순위 전도(Priority Inversion)라고 부른다. 이 글 뒷부분에서 더 자세히 다룰 것이다.

비록 GCD는 하위 레벨 C API이지만, 사용하는데 있어서 직관적이다. 디스페칭 블럭을 GCD의 큐에 적용하므로써 병렬처리 프로그램에서 발생하는 어려움들과 알고 있는 문제점들을 생각나지 않게 된다. 병렬 프로그래밍에서 발생 가능한 문제들에 대한 내용을 서술한, 해당 글의 병렬처리 프로그래밍이 처한 난관들을 꼭 읽어보기 바란다. 더 나아가 다음 포스팅에서는 GCD를 통해 이러한 어려운 난관들을 어떻게 해결할 수 있는지 알아 볼 것이다.

 

Operation Queue

Operation 큐는 GCD API 기반으로 만들어진 추상화 된 코코아의 큐 모델이다. GCD는 Operation 큐에 비해 낮은 레벨에서 컨트롤을 할 수 있게 해주지만, Operation 큐는 어플리케이션 개발자들에게 보다 안전하고 편리한 기능을 제공한다.

NSOperationQueue는 메인 큐(main queue)와 커스텀 큐(custom queue)로 구분 할 수 있다. 메인 큐는 메인 쓰레드 위에서 돌며. 커스텀 큐는 백그라운드에서 진행이 된다. 어떤 경우든, 해당 큐에서 진행되는 과업들은 NSOperation을 상속받은 서브 클래스들이다.

You can define your own operations in two ways: either by overriding main, or by overriding start. The former is very simple to do, but gives you less flexibility. In return, the state properties like isExecuting and isFinished are managed for you, simply by assuming that the operation is finished when main returns.

사용자는 main 메서드를 오버라이드 한다든지, start 메서드를 오버라이드하는 방식으로 두 가지 방법으로 자신만의 operation을 정의할 수 있다. 첫 번째 방식(main 메서드를 오버리이드 하는 방식)은 간단하지만 사용자가 할 수 있는 것이 제한 되어있다.

  1. @implementation YourOperation
  2.     – (void)main
  3.     {
  4.         // do your work here …
  5.     }
  6. @end

만약 좀 더 많은 컨트롤을 원하거나, 비동기식 작업을 해당 operation에서 실행하기 원한다면 start 메서드를 오버라이드하면 된다.

  1. @implementation YourOperation
  2.     – (void)start
  3.     {
  4.         self.isExecuting = YES;
  5.         self.isFinished = NO;
  6.         // start your work, which calls finished once it's done …
  7.     }
  8.  
  9.     – (void)finished
  10.     {
  11.         self.isExecuting = NO;
  12.         self.isFinished = YES;
  13.     }
  14. @end

만약 start 메서드를 오버라이드 해서 사용하고자 할때 주의 할 점은 operation의 상태를 수동으로 관리해야한다. Operation 큐가 큐 안에 있는 Operation을 선택하여 실행하기 위해서는 KVO 방식을 사용하여 상태 프로퍼티 값을 설정해주어야 한다. 그러므로 만약 디폴트 접근자를 사용하지 않고 있을 경우에는, 올바르게 해당 KVO 메세지를 보내고 있는지 주의해야 할 것이다.

Operation queue에서 긴 작업을 위해 제공하는 작업 취소하기 기능을 제대로 사용하기 위해서는 ‘isCancelled’ 프로퍼티를 주기적으로 확인해야 한다.

  1. - (void)main
  2. {
  3.     while (notDone && !self.isCancelled) {
  4.         // do your processing
  5.     }
  6. }

사용자가 Operation 큐를 정의만 해놓으면 큐에 넣고 사용하는 것은 매우 쉽다.

  1. NSOperationQueue *queue = [[NSOperationQueue alloc] init];
  2. YourOperation *operation = [[YourOperation alloc] init];
  3. [queue  addOperation:operation];

반면, 클래스를 정의해서 사용하지 않아도 블럭을 큐에 삽입해서 작업 할 수 있다. 이 방식은 간편하다. 예를 들어 사용자가 한번만 사용할 작업이라면 다음 코드와 같이 큐에 넣어 사용하면 된다.

  1. [[NSOperationQueue mainQueue] addOperationWithBlock:^{
  2.     // do something…
  3. }];

비록 위 코드와 같이 블록을 사용하여 스케쥴링하면 매우 편리하지만, NSOperation을 상속 받아서 작업하는 방식은 디버깅에 매우 유용하다. 사용자가 만약 description 메서드를 오버라이드하면 현재 특정 큐 안에 있는 operations들을 쉽게 알수 있기 때문이다.

operation이나 블록을 스케쥴링하는데 있어서, GCD에서 제대로하기 쉽지 않은 몇가지 작업을 operation 큐에서 제공해주고 있다. 예를 들어 maxConcurrentOperationCount 프로퍼티를 사용하여 특정 큐에서 병렬처리 할 과업의 수를 관리하기에 용의하다. 시리얼 큐에 설정을 할 시 다른 작업과 격리시키기 위한 목적으로는 사용할 수 있다.

또 다른 편리한 기능으로는 우선순위에 따라 특정 큐의 작업들의 정렬하는 기능이다. 이 기능은 GCD의 큐의 우선순위와 다른 기능이다. 오로지 하나의 큐에 스케쥴 잡힌 과업들의 실행에 영향을 주는 기능이다. 만약 다섯 가지 우선순위를 넘어서, 실행 순서가 정해져 있는 작업을 해야 한다면, 작업 간에 의존도를 부여해 줌으로써 원하는 순서에 실행을 시킬수 있다.

  1. [intermediateOperation addDependency:operation1];
  2. [intermediateOperation addDependency:operation2];
  3. [finishedOperation addDependency:intermediateOperation];

이 코드는 intermediateOperation를 operation1과 operation2가 끝내고서 실행이 되도록 의존도를 설정해 주고 있으며, finishedOperation은 intermediateOperation이 끝나면 실행이 될 것이다. Operation 의존도 실행 순서를 잘 정의해 주는 대단히 강력한 매카니즘이다. 이것은 사용자가 의존하고 있는 operation이 끝나야지만 실행 할 수 있도록 보장해 주는 operation 그룹 같은 것을 만들수 있게 해준다.

추상화의 기본적 특성 때문에 operation 큐는 GCD API와 비교하면 성능이 약간 떨어진다는 점이 있다. 하지만 대부분의 경우에는 임팩트가 약하며, operation 큐를 선택 사용하는 것이 좋은 선택이라고 생각한다.

Run Loops

런 루프는 과업들을 병렬실행 해주지 않기 때문에 기술적으로 GCD나 Operation 큐와 같은 병렬처리 매카니즘은 아니다. 하지만 런 루프는 직접적으로 메인 operation 큐와 메인 dispatch에서 실행되는 과업들과 연관이 있으며, 비동기식 코드 실행에 관한 매카니즘을 제공한다.

런 루프는 GCD나 Operation 큐를 사용하는 것 보다 헐씬 더 쉽게 사용할 수 있다. 왜냐하면 사용자는 병렬처리 대한 복잡한 것을 고려하지 않아도 비동기식으로 일을 해낼수 있기 때문이다.

런 루프는 언제나 하나의 특정한 쓰레드에서 작동된다. 메인 쓰레드와 연관되어 있는 메인 런 루프는 UI, 이벤트, 타이머, 그리고 커널 이벤트를 처리하기 때문에 코코아와 코코아 터치 어플리케이션에 중심적인 역할을 하고 있다. 사용자가 타이머를 사용하여 스케쥴를 잡거나, NSURLConnection을 사용하거나, 혹은 performSelector:withObject:afterDelay: 메서드를 호출 하면, 비동기식 작업을 처리하는 런 루프가 사용된다.

런 루프를 사용할 때마다 기억해야 할 것은 런 루프는 여러가지 모드로 실행 할 수 있다는 것이다. 각 모드는 런 루프가 반응하게 될 이벤트들을 정의한다. 이 방식은 메인 런 루프에 있는 과업들을 넘어 임시적으로 특정 과업들에게 우선순위를 정할 수 있는 좋은 방식이다.

이것에 대한 iOS의 예로 스크롤링을 될 수 있다. 사용자가 스크롤링을 하는 동안 런 루프는 디폴트 모드로 작동하지 않는다. 그렇기 때문에 예를 들어 타이머는 반응하지 않을 것이다. 스크롤링이 끝나면 런 루프는 디폴트 모드에 반환하고 큐에 적소된 다음 이벤트를 실행하게 될 것이다. 만약 스크롤링 중에 타이머가 작동하기 원한다면 런 루프의 NSRunLoopCommonModes에 추가해야 할 것이다.

메인 쓰레드는 항상 메인 런 루프를 설정하고 실행한다. 다른 쓰레드는 기본적으로 런 루프가 설정되어 있지 않는다. 사용자는 다른 쓰레드에도 런 루프를 설정할 수 있지만, 이런 경우는 거의 드물 것이다. 대부분은 메인 런 루프를 사용하는 것이 쉽다. 만약 메인 쓰레드에서 오래 걸리는 과업을 실행하고 싶지 않다면, 메인 런 루프로 부터 다른 큐에 넣어서 작동하게 하면 될 것이다. 크리스가 쓴 블로그 포스트에서 좋은 예재를 소개할 것이다.

그래도 만약 다른 쓰레드에 런 루프를 설정하여 사용하고 싶다면, 하나 이상의 입력 소스를 추가하는 것을 잊지 말아야 할 것이다. 만약 런 루프가 입력 소스가 설정되어 있지 않으면, 매번 실행하고 할 때 갑자기 종류될 것이다.

병렬처리 프로그램이 직면한 어려움들

병렬처리 프로그래밍을 작성하는 데는 많은 어려움이 따른다. 아무리 기본적인 작업을 하려고 해도 상호작용하는 여러 과업들이 병렬처리 될때 각각의 다른 상태를 감독하는 것이 어려워진다. 문제들은 병렬처리 코드를 디버깅하기 어려운 결정되지 않은 방식으로 나타다는 것이다.

지금부터 소개할 것은 병렬처리 프로그래밍의 예견치 못한 작동에 대한 예이다. 1995년 NASA는 화성에 정찰기를 보냈다. 탐사선이 화성에 성공적으로 착륙한지 얼마 지나지 않아 미션은 거의 실패로 돌아갈 위기에 처했다. 화성 탐사선은 원인불명으로 계속적으로 재 부팅이 되기 시작한 것이다. 낮은 우선순위 작업이 높은 우선순위 작업을 지속적으로 블록킹하여 나타나는 ‘우선순위 역전’ 현상으로 어려움을 겪고 있는 것이었다. 우리는 더 자세한 ‘우선순위 역전’ 현상에 대해서는 알아 볼 것이다. 그러나 이 예가 말해주고 있는 것은 엄청난 자원과 뛰어난 엔지니어들에게도 생긴 똑같은 어려움들이 당신에게 여러가지 방식으로 다가올 것이라는 것을 말해주고 있다.

공유자원

멀티 쓰레드에서 발생하는 수 많은 문제들의 근본지는 공유자원에 접근이다. 여기에서 말하는 자원은 클래스의 프로퍼티나 객체, 혹은 보통 경우 메모리, 네트워크 기기, 파일 등이 될 수 있다. 멀티 쓰레드 사이에서 공유하는 모든 것이 잠재적인 문제점이라고 할 수 있다. 그리고 반드시 안전한 측정을 취함으로 이러한 문제점들을 피해야 할 것이다.

소개한 문제를 재현해보기 위해 카운터로 사용하는 정수 프로퍼티 방식으로 되어 있는 자원을 예로 들어 보자. A와 B라고 하는 두 개의 쓰레드가 카운터를 동시에 평행하게 돌아간다고 가정해 보자. 문제는 C나 오브젝티브 C의 작성된 코드는 그냥 하나의 CPU의 기계식 명렁어 아니라는 것이다. 카운터의 숫자를 하나 올리기 위해서는, 현재 카운터의 값을 메모리에서 부터 불러와야 할 것이고, 해당 값을 하나 올린 다음에 다시 메모리에 써야할 것이다.

두 쓰레드가 동시에 처리하려고 시도하는 것을 상상해보라. 예를 들어, 쓰레드 A와 쓰레드 B가 17인 카운터의 값을 메모리로 부터 읽었다고 가정해보자. 쓰레드 A가 카우터의 값을 하나 올린 다음 18를 메모리에 쓰고, 쓰레드 B도 값을 하나 올린 다음 18이라는 값을 메모리에 썼다고 생각해 보라. 이 해당되는 데이터는 두번 값을 올렸음에도 실질적으로 1 밖에 안올라가는 오류가 생긴 것이다.

Screen Shot 2015-10-06 at 11.15.10 PM

이런 문제를 경함 조건(race condition)이라고 부르며, 멀티 쓰레드 환경에서 특정 쓰레드가 작업이 끝나기도 전에 다른 쓰레드가 동시에 자원에 접근할 때 항상 발생한다. 이런 상황을 막기 위해서는 멀티 쓰레드는 상호 베타적인 방식으로 공유자원에 접근해야 한다.

현실에서는 현존하는 CPU들이 최적화라는 이유로 읽기-쓰기 순서를 계속적으로 바꾸기(Out of order execution) 때문에 예재 보다 헐씬 복잡하다.

상호 베타

상호 베타적 접근이라는 뜻은 특정 자원에 한번에 하나의 쓰레드만 접근한다는 것을 말한다. 이것을 보장하기 위해서는 공유 자원에 접근하고자 하는 쓰레드는 먼저 뮤텍스 락을 획득하는 것이 필요하다. 자신의 작업이 끝나면 락을 풀어줌으로써 다른 쓰레드가 접근할 수 있는 기회를 얻어야 한다.

Screen Shot 2015-10-06 at 11.26.27 PM

상호 베타적 접근을 보장하기 위해서 추가적으로, 락은 반드시 순서 바꾸기 실행(out-of-order execution)을 통해 생기는 문제점들을 처리해야 한다. 프로그램 명령에 의해 정의된 순서에 따라 CPU가 메모리에 접근하는 것만 의지 해서는 부족하다. 사이드 이팩트를 피하는 방법은 메모리 방벽(barrier)을 사용하는 것이다. 메모리 방벽을 설정하면 순서 바꾸기 실행이 방벽에서는 생기지 않는다.(Setting a memory barrier makes sure that no out-of-order execution takes place across the barrier.)

물론 뮤텍스 락를 구현하는 것 자체로 경합 조건을 피할수 있다. 간단한 일이 아니며 현존하는 CPU에 대한 특별조취를 사용하는 것이 요구된다. 더 자세한 내용을 알고 싶다면 데니엘의 낮은 수준의 병렬처리 테크닉에 대한 글를 읽어보기 바란다.

Objective-C는 프로퍼티 선언시 atomic이라는 키워드 사용을 통해 언어적 레벨의 락 기능을 지원하고 있다. 프로퍼티를 atomic으로 선언하는 것은 공유자원의 매 접근시도 시 락과 언락을 해주는 결과를 가져다 준다. 만약을 위해 모든 프로퍼티를 atomic으로 선언해 줄수도 있지만 항상 비싼 비용이 든다는 것을 생각해야 한다.

자원에 락을 건다는 것은 언제난 성능적 비용든다. 락을 걸고 푸는 것은 멀티코어 시스템에서 구현하기 쉽지 않은 경함조건을 없는 것이 필요하다. 또한 락을 걸면 쓰레드는 먼저 자원은 사용하고 있는 다른 쓰레드 때문에 기달려야 한다. 이런 경우에는 sleep에 빠지게 되고 사용하던 쓰레드의 작업이 끝나면 노티를 받게 된다. 이모든 처리는 비용이 비싸고 어렵다.

또 다른 종류의 락이 있다. 어떤 락은 매우 저렴하지만 자원 경쟁에 약하다. 어떤 락은 기본적으로 비용이 비싸지만 자원 경쟁에서 강력하다.

락을 걸고 푸는 것에는 장단점이 있다. 그러므로 critical 섹션에 자주 들락거리는 것은 삼가해야 할 것이다. 동시에, 긴 실행 코드로 인해 락을 오래 잡는 것은 자원을 쓰고자 하여 락을 걸길 원하는 다른 쓰레드의 일을 하지 못하게 할 수도 있다. 참 어려운 문제이다.

동시에 실행하도록 짜여진 코드는 쉽게 접할 수 있지만, 공유 자원을 락하는 방식이 정해져 있어서 실제로 결과적으로는 하나의 쓰레드만 활성화 되는 결과를 낳는다. 사용자는 CPU 코어가 그들의 코드를 언제 스케쥴 잡아 사용할지를 판단하기 쉽지 않다. XCode의 Instrument의 CPU strategy 뷰를 통해 CPU 코어 할당을 효율적으로 사용하고 있는지 알아 볼수는 있을 것이다.

Dead Locks

뮤텍스 락은 경합조건을 해결해주지만 동시에 데드락이라는 새로운 문제도 만들어 낸다. 데드락은 하나 이상의 쓰레드가 다른 쓰레드의 작업이 끝나길 서로 기다리면서 생기는 현상이다.

Screen Shot 2015-10-13 at 1.22.50 PM

두 값을 스와핑하는 다음 예제를 한번 살펴보자.

  1. void swap(A, B)
  2. {
  3.     lock(lockA);
  4.     lock(lockB);
  5.     int a = A;
  6.     int b = B;
  7.     A = b;
  8.     B = a;
  9.     unlock(lockB);
  10.     unlock(lockA);
  11. }

위 코드는 거의 항상 잘 돌 것이다. 하지만 쓰레드가 동시에 상대방의 값으로 호출할 때는 발생하게 된다.

  1. swap(X, Y); // thread 1
  2. swap(Y, X); // thread 2

위와 같이 샐행하면 데드락에 빠질 것이다. 쓰레드 1은 x에 락을 걸것이고, 쓰레드 2는 y에 락을 것 것이다. 이렇게 된다면 두 쓰레드는 서로의 락이 풀리길 기다릴 것이며, 영원히 끝나지 않을 것이다.

다시한번 말하지만, 공유자원을 많은 쓰레드에 공유하거나, 락을 더 많이 걸 수록, 데드락에 빠질 위험을 더 커진다. 그렇기 때문에 최대한 간단하게 코딩을 해야 할 것이며, 쓰레드 사이에 공유자원을 최소화 해야한다. 로우 레벨의 병렬처리 API에 관한 글도 꼭 읽어 보길 바란다.

Starvation

고려해야 하는 상황이 많다고 생각할지 모르지만 새로운 난제를 하나 더 소개하고자 한다. 공유자원을 락하는 것으로 인해 읽기-쓰기 문제를 초래할 수있다. 많은 경우에 자원을 읽기 위한 접근을 한번에 하나씩 하는 것은 낭비일지도 모른다. 그러므로 리소스에 쓰기 락이 없는 한 읽기 락을 허용하는 경우가 많다. 이럴 경우에는 한 쓰레드에서 쓰기위한 락을 걸기위해 읽기위한 락들이 풀리길 기다리다가 굶줄이는 경우가 생긴다.

이 문제점을 해결하기 위해서는 읽기-쓰기 락과 같은 간단한 방법보다는 현명한 솔루션이 필요하다. 예를 들어 writer preference를 주거나 읽기-복사하기-갱신하기 알고리즘(read-copy-update algorithm)을 사용한다. 데니엘이 그가 쓴 로우 레벨 병렬처리 기법에 관한 글을 통해 GCD를 사용한 독자/싱글 writer 패턴을 어떻게 구현하는지 알려 줄것 이다.

우선순위 역전현상

우리는 이 글을 초반부에 NASA의 화성 탐색선이 겪은 병렬처리 관련 문제로 시작하였다. 지금부터 우리는 왜 탐색선이 거의 실패로 돌아 갈 뻔했는지 알아 볼것이며, 왜 우선순위 역전현상이라 불리는 이 어려움을 우리도 똑같이 당할수 있는지 알아 볼 것이다.

우선순위 역전현상은 낮은 우선순위 과업이 높은 우선순위 과업의 실행을 막는 상황를 말하는데, 효율적 진행을 위해 우선순위가 역전되는 것을 말한다. GCD가 파일 I/O를 위한 큐를 포함해 백그라운드 큐를 다양한 우선순위를 가지고 제공되고 있는 지금 꼭 알아두어야 할 중요한 내용이라고 생각한다.

해당 이슈는 높은 우선순위 과업과 낮은 순위 과업이 공유자원을 함께 사용할 때 생길 수 있다. 낮은 우선순위 과업이 공유자원을 사용하기 위해 락을 걸었다면, 높은 우선순위의 과업이 공유자원을 사용하고자 할 때 아무런 거리낌 없이 사용할 수 있도록 락을 일찍 풀어야 한다. 낮은 우선순위 과업이 자원을 점유하고 있으므로 높은 우선순위 과업이 블럭당하면, 그 사이에 큐 안에서 실행 가능한 과업 중 가장 높은 중간정도 되는 우선순위 과업이 높은 우선순위 과업 보다 앞서 실행될 가능성을 열어주게 된다. 이때 중간 우선순위 과업이 낮은 우선순위 과업의 락을 풀는 것을 방해하여 높은 우선순위 과업은 계속 공유자원의 락이 풀리길 기다리게 되여 실행하지 못하게 된다.

Screen Shot 2015-10-13 at 1.35.50 PM

당신의 코드에서는 화성의 탐사선이 재부팅을 하는 것 만큼 심각한 상황을 초래하지 않을 수 있지만 우선순위 역전현상은 꽤 자주 발생하는 현상이다.

특별한 경우가 아니면 서로 다른 우선순위 큐를 사용하지 않길 권유한다. 가끔 높은 우선순위 과업이 낮은 우선순위 과업이 끝날 때 까지 기다리는 현상이 이럴날 수 있기 때문이다. GCD를 사용할 때, 디폴트 우선순위 큐를 사용하길 권한다. 만약 다른 우선순위 규를 사용하게 되면, 상황을 더 악화시킬 수 있다.

하나 이상의 큐를 사용할 때 각기 다른 우선순위를 설정하여 사용하는 것은 좋아 보이지만, 병렬처리 프로그래밍에서 ‘우선순위 역전현상’과 같은 과거의 경험을 통해 보면, 복잡성을 높이게 되며 예상할 수 없는 상황을 만들게 된다. 만약 앞으로 개발을 하면서 높은 우선순위 과업이 예상한 타이밍에 실행되지 않을 때 NASA의 전문 엔지니어도 경험한 ‘우선순위 역전현상’을 떠올리게 될 것이다.

마치며…

우리는 API가 아무리 사용하기 쉽게 보일지라도 해당 글을 통해 병렬처리 프로그래밍의 복잡성을 알려주길 원했고 그에 수반한 문제점을 알아 보길 원했다. 이러한 문제들은 가지고 오는 결과들은 어디가 문제인지 쉽게 파악하기 어렵고, 디버깅하기 조차 매우 어렵다.

반면에 병렬처리 기술은 멀티 코어 컴퓨팅의 이점을 사용할 수 있는 매우 강력한 도구이다. 성공의 열쇠는 병렬처리 모델을 최대한 간단하게 유지하여 락을 필요할 때만 걸라고 하는 것이다.

우리가 추천하는 안전한 방법은 메인 쓰레드에서 데이터를 뽑아내서 백그라운드 큐에서 실제 처리를 하고 결과를 다시 메인 쓰레드에 전달하라는 것이다. 이 방식은 락을 걸지 않게 되하여 실수를 줄일 수 있을 것이다.

[iOS] 로컬라이즈 텍스트 하기 (Localized String)

앱을 만들 때 이제는 국제화를 고려하지 않을 수 없는 시대가 되었다. 앱에서 사용되는 텍스트를 국제화하기 위해 애플에서는 Localization과 Internationalization을 제공하는데 이 블로그를 통해 Localization하는 방법을 간단히 알아본다.

 

A. 세팅하기.
1. 새 파일 추가한다. iOS->Resource->String File. 파일명은 ‘Localizable.strings’ 설정.
2. 추가하고 싶은 언어를 Project->Info->Localization에서 선택한다. (사진참조)
Screen Shot 2015-01-26 at 10.58.28 AM

3. Localizable.strings 파일을 선택하고, File Inspector 창을 열어서 Localize 버튼을 클릭한다. 그리고 나서 Drop down UI에서 ‘English’를 선택한다. (사진참조)

Screen Shot 2015-01-26 at 11.02.36 AM

4. 추가를 하면 오른쪽에 위치한 File Inspector의 Localization 항목에 English가 선택되어 있을 것이다. 새로 추가한 언어도 클릭하여 추가한다. (사진참조)

Screen Shot 2015-01-26 at 11.06.38 AM

 

5. 왼쪽 새로 추가된 Localizable.strings(Spanish)를 클릭하여 Key/Value형식으로 파일을 작성하면 세팅은 끝이다.

Screen Shot 2015-01-26 at 11.08.04 AM

 

B. 사용하기.
1. 파일 안에 형식은 “KEY” = “CONTENT”; 형식으로 한다.
2. 코드안에서 사용할 때는 ‘NSLocalizedString(@”"), nil)’ 메크로를 호출하여 사용한다.

[iOS] Keychain 기본 개념과 Wrapper Class 제공

Keychain은 무엇인가?

Keychain은 Mac OS X, iOS의 다양한 응용 프로그램에서 사용되는 비밀번호를 저장하는 암호화 되어 있는 저장소이다. 그런데 이것이 왜 필요한가 궁금해 할지 모른다. 예를 들어보자면, 맥북 사용자 혹은 iOS 기기 사용자는 여러가지 앱들을 사용한다. 메세지 앱, 소셜 앱, 유틸 앱, 금융 앱 등등. 각 앱마다 혹은 각 서비스를 사용하기 위해서는 ‘사용자 인증’ 과정이 필요하다. 본인임을 인증하는 과정은 현재 2015년 1월까지도 대부분이 ID & Password 방식으로 인증하는 방식을 사용하고 있다. 그렇기 때문에 각각의 비밀번호를 외우고 있어야하는 번거러움이 생긴다. 모든 서비스에 같은 비밀 번호를 설정하든지, 혹은 각 서비스 별로 사용하는 비밀번호를 안전한 곳에다가 적어두고 사용할 때마다 꺼내 보든지 해야하는데, 이러한 번거러움을 해결하고자 만들어진 것이 바로 Keychain이다. Macbook 사용자들은 Keychain에 저장되어 있는 비밀번호를 사용하기 위해서 컴퓨터 root 권한의 비밀번호를 치기만 하면 각각 앱, 혹은 서비스에서 Keychain에 저장한 오래되어 기억나지 않는 비밀번호를 사용할 수 있다. iOS 같은 경우에 Keychain은 각 앱에서 저장한 정보를  가지고 올 수 있는데, Provisioning profile 별로 사용경로가 구분이 된다. 쉽게 말하자면 같은 앱이라 할찌라도 만약 개발 단계에서 Profile이 바뀐다면 그전에 Keychain에 저장해 둔 정보를 빼올 수 없을 것이다.

 

언제 Keychain을 사용할 것인가?

앱 개발자 입장에서 생각해보자. 기기에 데이터를 저장을 할 수 있는 여러가지 방법이 존재한다. 각자의 경험에 따라 떠오르는 것이 다르겠지만,  기본적으로는 데이터베이스 혹은 Core Data, NSDefaultUser, Keychain, .plist 파일 등등. 저장할 방법도 다양하며 각각마다 만들어진 목적이 다르다. 그럼 Keychain은 언제 사용할 것인가? Keychain의 가장 큰 장점은 ‘보안’이다. 그렇기 때문에 사용자의 소중한 정보를 저장하고자 한다면 Keychain을 사용하는 것이 옳다. (분명 다른 매체를 통해서 데이터 저장이 가능하다. 분명히 해둘 것은 불가능한 것이 아니라 비밀번호, 혹은 각종 서비스의 API를 사용할 때 사용되는  secret Key, 혹은 Access Token 같은 보안이 중요한 데이터는 암호화를 하여 저장 관리하는 Keychain에 다가 저장하는 것이 옳은 방법이라는 것이다.)

 

어떻게 Keychain 서비스를 사용하는가?

애플에서는 Keychain 서비스 사용방법을 설명하기 위해 샘플소스를 공개하고 있다. Stack Overflow에 관련 질문에 대한 도 있으니 한번 살펴보기 바란다. 그리고 개인 Github에 Swift용과 Objective-C용으로 올려놓았으니 한번 사용해보기 바란다. 헤더 파일은 아래와 같이 간단하다. 저장하고자 하는 정보가 있다면 key와 함께 NSData 형식으로 넘기면 된다.

  1. @interface KeychainHelper : NSObject
  2.  
  3. + (BOOL)saveWithKey:(NSString *)key andData:(NSData *)data;
  4. + (NSData *)loadWithKey:(NSString *)key;
  5. + (BOOL)deleteWithKey:(NSString *)key;
  6. + (BOOL)clear;
  7.  
  8. @end

[Facebook-Tweaks] 간단하게 살펴보기.

Tweaks Framework by FB

이틀 전 (May.24.2014) 페이스북에서 iOS 프래임워크 하나를 Github에 올리면서 공개를 했다. 해당 프래임워크의 주 기능은 간단하다. 완성된 output의 사소한 속성들을 변경하고 싶을 때 개발자의 손에 걸치지 않고 Runtime으로 수정 가능한 기능을 제공해 준다는 것이다. (아마 무슨 뜻인지 이해가 가질 않을 것으로 생각된다.)

 

“Why do I need it?”

예를 들어 개발자가 애니메이션의 Duration을 정해서 컴파일을 할 것이다. 그런데 모션 그래픽 디자이너라든지 기획자가 보기에 너무 길다던가 맘에 안들 수 있다. 그런데 일일이 개발자 옆에 앉아서 테스트 해보거나 혹은 Bug Tracking System 같은 경로를 통해서 이와 같은 다양한 주문을 시도할 것이다. 하지만 이런 과정은 프로젝트 전체를 보았을 때 상당히 값이 비싼 비용이다. 개발자와 디자이너와 신경전을 한 두번 해본 개발자라면 이 프래임워크의 큰 가치를 깨달을 것이다.

 

“What is ‘Tweaks’?”

Tweaks는 이런 문제점을 해결해주는 좋은 대안이 될 수 있다. 프래임워크를 사용하여 디자이너 혹은 기획자가 직접 UI의 속성을 바꿔가면서 테스트해볼 수 있는 대안이 생겼기 때문이다.

from github

위 gif 이미지를 잠시 보자. ‘Tweaks’라는 UILabel 객체가 화면에 있다. 그런데 어떤 이유에서든지 디자이너가 색상이 맘에 들지 않아서 이를 변경하고자 한다. 일반적으로 디자이너는 개발자에게 이메일을 보내거나 가끔은 자리에 찾아와 색상 변경을 요구하며 이것 저것을 테스트 해보길 요구할 것이다. 이럴 때에 개발자에게 있어서는 얼마나 방해가 되는 일인지 모르며 이런 작은 방해들이 점점 모여서 프로젝트의 비용을 점점 비싸게 만드는 것이다. 그런데 (개발하기 나름이겠지만… ) 본 프래임워크를 적용해서 폰을 흔들면 UI 속성을 바꿀수 있는 TableView가 나온다. 그럼 해당 UI의 속성을 바꿈으로 개발자의 손을 빌리지 않고 이것 저것 tweaking을 할 수 있는 길이 생긴 것이다.

 

“How to use?”

github 페이지에 설치 방법 부터 사용 방법이 잘 나와 있다. 가장 간단히 설치할 수 있는 방법은 CocoaPod를 사용하는 것이다. 소스코드의 순서를  간단하게 보면 다음과 같을 것이다.

  1. 프래임워크 Import 함.
  2. FBTweakViewController를 화면에 뿌릴수 있는 장치를 만듬.
  3. UI 객체 인스턴스의 속성에 바인딩 혹은, ‘FBTweaksValue’를 통해  설정 값 할당 함.

 

프래임워크 Import 하기

  1. #import <FBTweak/FBTweak.h> //FBTweak 기본 헤더파일
  2. #import <FBTweak/FBTweakShakeWindow.h> //폰을 흔들어 FBTweaksViewController를 열고 싶을 때 사용하기
  3. #import <FBTweak/FBTweakInline.h> //FBTweakInline 객체 사용할 때 필요
  4. #import <FBTweak/FBTweakViewController.h> //FBTweakViewController를 presentViewController:animated:completion:을 통해 열때 필요

 

FBTweakViewController를 화면에 뿌릴 장치

변경 가능한 UI 속성을 제어할 UITableView를 화면에 나타내주기 위해서 크게 두 가지 방법이 있을 것이다. 첫 번째는 폰을 흔들어서 나타나게 하는 방법과 임시의 버튼을 만들어서 presentViewController:animated:completion: 메서드를 사용하여 FBTweakViewController 객체를 화면서 나타나게 하는 방법이다. 둘 중에 편한 방법으로 사용하면 될 것이라고 생각 되어진다.

 

UI 속성 바인딩 혹은 설정 값을 Tweaks 함수를 통해 할당

이제 자신이 디자이너들이나 타인들에게 Runtime 때에 변경 가능한 UI 속성을 주고자 하는 코드에 FBTweakValue 함수를 통해서 값을 할당 하든지, FBTweakBind 함수를 통해서 값을 바인딩하면 된다.

  1. //#. Binding 예
  2. UILabel *_label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, _window.bounds.size.width, _window.bounds.size.height * 0.75)];
  3. _label.textAlignment = NSTextAlignmentCenter;
  4. _label.numberOfLines = 0;
  5. _label.userInteractionEnabled = YES;
  6. _label.backgroundColor = [UIColor clearColor];
  7. _label.textColor = [UIColor blackColor];
  8.  
  9. //1-a
  10. _label.font = [UIFont systemFontOfSize:FBTweakValue(@"Content", @"Text", @"Size", 60.0)];
  11.  
  12. //1-b. UILabel 객체의 텍스트 속성을 동적인 값으로 바인딩
  13. FBTweakBind(_label, text, @"Content", @"Text", @"String", @"Tweaks");
  14.  
  15. //1-c. UILabel 객체의 알파 속성을 동적인 값으로 바인딩
  16. FBTweakBind(_label, alpha, @"Content", @"Text", @"Alpha", 0.5, 0.0, 1.0);

‘FBTweakBind’ 함수 사용법은 다음과 같다.

  1. FBTweakBind(object_, property_, category_, collection_, name_, …)

  • object: UI 오브젝트 인스턴스
  • property: 객체 속성명
  • category: TableView에 나올 TableViewCell 명
  • collection: grouped tableView에 표시 될 그룹 명
  • name: 해상 TableViewCell에 표시 될 title 명
  • …: 기본 설정 값

‘FBTweakValue’ 함수 사용법은 다음과 같다

  1. FBTweakValue(category_, collection_, name_, …)

  • category: TableView에 나올 TableViewCell 명
  • collection: grouped tableView에 표시 될 그룹 명
  • name: 해상 TableViewCell에 표시 될 title 명
  • …: 기본 설정 값

그리고 나서 뒤에 오는 값들은 기본 설정 값이다. 이 기본 설정에 따라 TableViewCell에 나타날 설정 가능하도록 해주는 UI가 결정이 된다. 1-b에는 NSString인 ‘Tweaks’로 설정되어 있고, 1-c에서는 세개의 값이 있다. 첫 번째 값인 0.5는 디폴트 값이며, 두번째는 설정 가능한 가장 낮은 값, 세번째 값은 설정 가능한 가장 큰 값이다. 이렇게 설정을 하면 UIStepper를 통해 설정이 가능하게 해준다. 만약에 이 값에 YES와 같은 Bool 값이 들어오면 Toggle 가능한 UISwitch가 들어 올 것이다.

Where to go next?

github에 올라온 소스를 자신의 컴퓨터에 클론하여 예제 앱을 사용해 보면 자신의 프로젝트에 어떻게 사용할지 쉽게 알게 될 것이다. 위에 설명한 것들은 정말 기본적이고 쉬운 것들이고, 더 advance한 사용은 github에 나와있는 설명과 stackoverflow를 통해 알아보기 바란다. 혹자도 기존 프로젝트에 적용해보고 추가할 내용이 생기면 추후에 본 블로그를 더 업데이트 하겠다.

method_exchangeImplementations으로 디버깅 가능한 메서드 만들기.

Objective-C는 메서드를 실행하는 행위를 ‘메세지를 보낸다’ 혹은, ‘메세징’이라고 부르기도 한다. 그런데 이 메세징 과정은 컴파일 타임에 정해지는 것이 아니라 런타임에 정해지므로 여러가지 기술과 다이나믹한 프로그래밍 기법을 통해 개발이 가능하다. 해당 글에서 살펴볼 기법은 Foundation 라이브러리와 같이 implementation을 볼 수 없는 불투명한(Opaque) 기존 메서드를 method_exchangeImplementations 함수를 통해 디버깅에에 유용한 Log를 찍을 수 있는 간단한 팁 하나를 소개하고자 한다.

모든 Implementation은 함수 포인터와 같은 IMPs라는 것으로 다음과 같은 프로토타입을 갖는다.

id (*IMP) (id, SEL,…)

Screen Shot 2014-01-17 at 4.25.53 PM

 

위 그림은 NSString의 세개의 메서드 selector가 각각의 IMP를 가리키는 모습의 다이어그램을 보여주고 있다. 그래서 만약 각 selector가 가지키고 있는 IMP를 아래와 같이 변경을 해준다면, 변경 요청 이후부터는 계속적으로 변경되어 가르키고 있는 IMP에게 메세지를 보낼 것이다.

 

Screen Shot 2014-01-17 at 4.29.53 PM

 

 

위의 예를 다음 코드를 통해 살펴보자.

 

  1. //1.
  2. #import <objc/runtime.h>
  3.  
  4. #pragma mark – Interface extension of 'NSString'
  5.  
  6. //2.
  7. @implementation NSString (Debuggable)
  8.  
  9. - (NSString *)debuggableLowercaseString
  10. {
  11.     NSString *lowercase = [self debuggableLowercaseString];
  12.     NSLog(@"%@ => %@", self, lowercase);
  13.  
  14.     return lowercase;
  15. }
  16.  
  17. @end
  18.  
  19. @implementation ViewController
  20.  
  21. #pragma mark – Effective Objective-C
  22.  
  23. - (void)methodSwizzlingToDebugOpaqueMethod
  24. {
  25.     //3.
  26.     Method orignMeth = class_getInstanceMethod([NSString class], @selector(lowercaseString));
  27.     Method swappedMeth = class_getInstanceMethod([NSString class], @selector(debuggableLowercaseString));
  28.  
  29.     //4.
  30.     method_exchangeImplementations(orignMeth, swappedMeth);
  31.  
  32.     //5.
  33.     NSString *test = [@"TEST STRING" lowercaseString];
  34. }
  35.  
  36. @end

 

코드 설명

  1. 함수 method_exchangeImplementations는 역시 “objc/runtime.h”에 포함 되어있다.
  2. 인터페이스 확장으로 NSString에 커스텀 함수를 하나 등록한다. debuggableLowercaseString를 보면 자신을 호출하는 재귀함수 처럼 보일지모르지만 사실은 메서드가 swap된 이후에 이 코드가 실행되기 때문에 lowercaseString의 IMP에게 메세지를 보낼 것이다.
  3. 함수 class_getInstanceMethod (클래스명, 셀랙터)를 통해 IMP 즉 함수 포인터를 얻을 수 있다.
  4. method_exchangeImplementations 함수를 통해 IMP를 swap할 수 있다.
  5. lowercaseString을 메세지 보내여도 실제로 debuggableLowercaseString의 implementation인 연결되었으므로 debuggableLowercaseString가 실행될 것이다.

 

마치며

본 포스트에서 살펴본 기술 또한 디버깅의 목적 이외에 다른 목적으로 사용할 수 있겠지만 아무곳에나 사용한다면 서스테인하기 힘들 것이며 디버깅에 힘들 것이다. 그러므로 주의해서 유용하게 사용하도록 하자.

[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 &lt;= 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 &lt;= 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 &lt;= 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 &lt;= 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 라이브러리를 써서 적용하려고 고생을 하다가. 좋은 글을 하나 찾아서 링크를 걸 어보았다.

링크

[WWDC 2013 - 3] Advances in Objective-C

안녕하세요. 바티입니다.

오늘은 WWDC 2013에서 발표한 Objective-C의 새로운 점들을 소개해드리고자 합니다. 본 포스팅은 WWDC 2013에서 발표한 자료를 기반으로 작성된 자료입니다.

Screen Shot 2013-09-04 at 2.51.31 PM

Road Map

  • Modules
  • Better productivity
  • ARC improvements

‘Advances in Objective-C’에서 발표는 내용을 큰 분류를 한다면 다음과 같이 크게 세가지로 나눌 수 있을 것이다.

 

#Modules

문제점

사실 개인적으로 이번 발표를 통해 지금까지 컴파일러가 Compile time 전에 전처리를 어떻게 하고 있었는지 알게 되었다. 사실 ‘매크로’와 같은 간단한 코드들만 한다고 생각했었다. 컴파일 전처리 때에 여러가지 일을 하겠지만 해당 주제와 관련있는 ‘import’ 부분을 더 자세히 살펴보도록 하자.

Simple Diagram shows how pre-compiler works

 

위 그림은 ‘MyApp.m’이라는 구현 파일에서 ‘iAd.h’(라이브러리)를 임포트 했다고 가정했을 때 컴파일 전처리를 어떻게 하는지 보여주는 다이어그램이다. 단순히 ‘.m’에 임포트 되어 있는 파일을 ‘복사/붙여넣기’ 식으로 컴파일 타임 전에 이루어진다고 한다. 그렇기 때문에 라이브러리 파일 하나를 임포트 했다고 해서 한 개만 ‘.m’에 붙여넣기가 되는 것이 아니라 임포트한 파일이 또 임포트 하고 있는 파일들을 복사하고 붙여넣는다고 한다. 그렇기 때문에 실질적으로 적은 량의 구현 파일이라 할찌라도 정작 컴파일 때에 붙어있는 파일의 크기는 우리가 생각하는 것 이상으로 어마어마하다는 뜻이다.

Screen Shot 2013-09-04 at 3.02.21 PM

위 그림의 파란 부분이 ‘.m’파일의 실제 구현 소스의 양이고 초록색과 보라색의 부분이 라이브러리를 임포트한 소스의 양이다. 파일 하나만 그런 것이 아니라 n 개의 파일이 존재한다면 엄청난 양이 될 것이며 컴파일 하는 시간이 왜 오래걸리는지 알게 될것이다.

하지만 지금까지 이런 방식으로도 개발을 잘 해오지 않았는가? 그렇다면 무엇이 문제일까? 세션 발표에서 소개한 문제점은 매우 ‘깨지기’ 쉽다고 한다. 예를 들어 다음 코드를 보자.

  1. #define readonly 0×01
  2. #import &lt;iAd/iAd.h&gt;
  3.  
  4. @implementation AppDelegate
  5. //…
  6. @end

‘readonly’ 키워드를 define을 통해 ’0×01′으로 대처하는 코드를 넣었다고 가정하자. 빌드를 하기 전까지는 아무런 문법적 에러가 발생하거나 발견되어지지 않는다. 하지만 컴파일 전처리 과정에서 ‘iAd’의 코드가 ‘.m’파일에 복사되면서 다음과 같은 일이 발생한다.

Screen Shot 2013-09-04 at 3.09.35 PM

 

@property 옵션으로 ‘readonly’가 ’0×01′로 대처되어 버려 에러가 나는 장면이다.

코드의 양도 늘어나 포퍼먼스적으로도 문제가 생기며, 컴파일 전에 발견하기 어려운 에러 발생률도 높다는 점이 지금까지 우리가 안고 온 약점이자 부족한 점이라고 할 수있다.

이것을 보안하기 위해서 Apple은 ‘Module’이라는 메커니즘을 가지고 나왔다.

해결책

Screen Shot 2013-09-04 at 3.13.30 PM

 

위 슬라이드와 같이 ‘Module’은 프래임 워크를 은닉하고 미리 따로 컴파일이 된다고 한다. 해당 세션에서 언급은 하지 않았지만, 다른 세션에서 언급하기로는 ‘Module’ 메커니즘을 사용하기 위해서 별도의 데이터베이스가 존재한다고 들었다. 컴파일 전처리 때에 기존처럼 임포트한 파일들의 내용을 ‘.m’ 구현 파일에 복사/붙여넣는 것이 아니고, 미리 컴파일 된 자원을 가지고 적용한다는 말이다.

Screen Shot 2013-09-04 at 3.16.58 PM

 

그렇기 때문에 위 슬라이드가 설명하는 것 처럼 로컬 매크로 정의는 프레임 워크 API에 영향을 주지 않는다고 한다.

뿐만 아니라 라이브러리를 임포트 할 때 전체 라이브러리를 꼭 임포트하지 않아도 된다고 한다. 부분적으로 선택적 임포트가 가능하다고 한다.

뿐만 아니라 라이브러리를 적용할 때는 항상 설정에 가서 라이브러리를 링크시켜 주어야 하는 번거러움이 있었다. 하지만 이젠 그런 수고도 하지 않아도 된다고 한다.

Autolinking

Autolinking

사용법

Screen Shot 2013-09-04 at 3.21.19 PM

 

사용법도 간단하다. 위 슬라이드를 보면 ‘#import’문을 ‘@import’으로 변경해주면 된다. 물론 빌드 세팅에서 설정을 해주어야하며 iOS 7이상과 OS X 10.9 SDK 이상을 적용해야 사용이 가능하다고 한다.

빌드 설정

빌드 설정

이렇게 적용을 했을 때 우리가 얻을 수 있는 효과는 빌드 타임과 인댁스 타임이 줄어든다는 것을 다음 슬라이드들을 통해 강조한다.

Screen Shot 2013-09-04 at 3.24.04 PM

Screen Shot 2013-09-04 at 3.24.46 PM

요약

  • 프래임워크 사용 용이
  • 소스 도구의 성능 향상
  • 기존 코드 바꿀 필요 없음

 

# Better Productivity

Apple에서 더 나은 생산성을 제공해주기 위해 다음과 같은 네 가지를 제공한다고 한다.

  • 코드 모던화를 위한 툴 제공
  • 향상된 SDK
  • 안정적인 블럭 반환 타입
  • 런타임

코드 모던화를 위한 툴 제공

Screen Shot 2013-09-04 at 3.33.57 PM

작년 WWDC 2012에서 발표한 새로운 문법을 지원하기 위해서 IDE의 Refactor 부분에 기능이 추가되었는데 이 기능들을 통해 ARC를 적용한 문법을 자동으로 변화 시켜주거나, 모던형식의 문법으로 변환 시켜주는 기능을 제공 해주고 있다. (모던 형식의 문법은 작년에 발표된 부분이기에 여기에서 다루지 않겠다.)

향상된 SDK

Screen Shot 2013-09-04 at 3.38.00 PM

 

발표한 세션에서는 ‘New features and you’부분의 두 가지를 조금도 조명하여 설명하고 있다. 새로운 기능이기 때문인 것으로 보인다.

‘instancetype’ 키워드

여러분들은 Xcode 4에서 다음과 같은 코드를 테스트 해본적이 있는가?

  1. NSDictionary *dic = [NSArray array];
  2. NSLog(@"%@", dic);

위 코드를 Xcode 4에서 할 때와 Xcode 5에서 할 때는 차이가 있다. Xcode 4에서는 경고 메세지를 보여주지 않는다는 점이다. 그 이유는 ‘NSArray’의 array 메세지는 +(id) array;와 같이 ‘id’로 반환 타입을 정의하고 있기 때문에 경고를 보여주지 않는다. 하지만 Xcode 5에서는 ‘id’ 대신 ‘instancetyupe’이라는 키워드를 사용하여 반환 타입을 정의하는데, 이 키워드는 다음과 같은 특징을 가지고 있다고 한다.

Screen Shot 2013-09-04 at 3.49.37 PM

 

 

Explicitly-Typed Enums

이 기능은 글로벌 Enum 변수를 구분해주는 방법이다. 기존에는 보통 Enum을 변수명은 다르게 만들어도 실제 값은 보통 1에서 10 사이의 숫자로 정의하는 것이 통상적이다. 그렇기 때문에 다른 Enum 타입이라도 할당을 하거나 사용을 할 때면 코드 작성 타임에서는 컴파일러가 다른 종류의 Enum인 것을 알아차리지 못했다. Enum을 정의하기 위해 기존 방식대로 한다면 다음과 같다.

  1. enum {ABC, JKL, XYZ};
  2. typedef NSUInteger MyEnum;

하지만 새로운 문법을 통해 코드 작성 타임에도 경고를 줄 수 있게 되었다.

  1. enum MyEnum : NSUInteger {ABC, JKL, XYZ};
  2. typedef MyEnum MyEnum;

Garbage Collection

가비치 컬랙션은 오직 ‘맥’에서만 작동을 한다. 하지만 ARC가 그의 역할을 대처를 한다. 그뿐 만이 아니라 OSX 10.8에서는 Deprecated될 것이다. 뿐만아니라 AVKit, Accounts, GameController, GameKit, MapKit, Social, SpriteKit, 등 새로운 프래임워크에서는 지원을 하지 않는다. 그러므로 되도록이면 ARC를 사용하기로 하자.

ARC의 업데이트와 발전

Automatic Reference Counting은 객체 포인터를 몇 번을 사용하고 있는지 파악한 후 메커니즘에 의해서 카운터를 해재해주는 메모리 관리 방법이라고 할 수 있다.

코코아 프레임워크는 의미적인 reference counting 방식으로 디자인 되어졌다. 결정적으로 객체의 해재에 대한 순서가 중요하다. 그리고 디버깅 작업에 매우 용이하다.

ARC는 뛰어난 코코아 코드를 작성할 수 있도록 도와준다. 메모리에 대한 염두를 뒤로하게 만들어 자신의 로직에 더 집중할 수 있도록 도와준다.

성능은 계속적으로 향상되어 가고 있으며, ‘__weak’ 레퍼런스는 약 두 배정도 iOS 7과 OSX 10.9에서 빠르다. 뿐만아니라 디버깅 때에 사용할 메모리가 예상이 가능하다.

ARC를 적용하기 위해서는 다음과 같은 사항을 지키면 된다.

  • ‘retain / release / autorelease’를 제거해준다.
  • 빈 dealloc 메서드를 제거한다.
  • NSAutoreleasePool를 @autoreleasepool로 변경한다.

 

마치며…

객체지향의 모습을 갖추기 위해서 열심히 노력하는 C 언어 진형에서는 가장 큰 고민 거리는 메모리 문제가 아닐까 싶다. 완전한 C라면 할당 받은 것은 해제를 시켜주어야하는 번거러움이 당연하지만, 객체지향적 프로그래밍에서는 그것이 쉽지 않기 때문에 가비치 컬랙션이니, Reference Counting 메커니즘이니 하는 것들이 나오는 것이다. 지금 버젼의 언어도 충분히 완벽에 가까운 객체지향적 언어가 되어 있는 모습인다. 이번 업데이트를 통해서 개발자에게 해당되는 ‘개발자 경험’에 최대한 멋진 경험을 주기위해 여러가지를 시도하고 적용한 모습이다. (예를 들어 ‘모듈’의 등장을 봐도 그렇다.)

‘개발자 경험’가 좋아진다면 개발 결과물이 좋아질 것이고, 개발 결과물이 좋아진다면 사용자에게도 좋은 경험을 제공할 것이 분명하다. 그렇기 때문에 ‘사용자 경험’을 중요시 하는 애플에서 끊임 없이 ‘개발자 경험’에 대한 연구를 하고 발전시키는 것이 아닌가 싶다.