[Summary] Effective Objective-C 2.0

A. Objective-C와 친해지자

 

1. Objective-C의 근본을 이해하라

- c 언어는 function calling, objective-c는 dynamic binding을 사용하는 messaging structure이다. 가장 큰 차이는 런타임 때에 어떤 객체의 함수를 호출할지 결정된다는 점이 다르다.
- Runtime은 무거운 집을 다 들고 있다고 보면 된다. 데이터 구조와 객체에 해당하는 함수에 대한 정보들이 이에 해당되며 메모리 관리 함수들도 포함되어 있다.
- Runtime은 모든 코드를 서로 연결해주며 dynamic library를 링크해준다. 그러므로 라이브러리가 업데이트되면 재 컴파일 없이 성능적 이점을 누릴수 있게 된다.
- objective-c의 객체는 항상 stack 메모리 영역이 아닌, Heap 메모리 영역에 쓰인다. (이를 가리키는 포인터 변수는 스택에 저장되자만 말이다.) 스택에 저장된 변수 포인터들은 32 비트 컴퓨터에서는 4 바이트를, 64 비트 컴퓨터에서는 8 바이트를 할당한다.
- Heap 메모리는 레퍼런스 카운팅이라는 매카니즘으로 Objective-C가 추상화 하여 관리한다. malloc이나 free 같은 함수를 사용하지 않아도 된다.

 

2. 헤더파일에 헤더파일 importing 최소화하기

- 헤더 파일에 import는 컴파일 타임 때에 해당 파일의 내용이 복사가 되어 첨부가 되어 진다. 그렇기 때문에 헤더 파일에 import 하는 행위는 최소화 해야한다.
- 헤더 파일에 @class 예약어를 사용하여 존재여부 정도만 알리고, 클래스의 자세한 내용을 감춘다. Implementation 파일에서는 import를 함으로써 클래스의 사용을 위해서 자세한 내용을 보여줘야 한다.
- @class를 하용하여 forward declaration을 사용하면 상호 참조하는 경우를 피할 수 있게 해주는 효과를 얻을 수 있다. 상호 참조를 할 때 #import를 #include 대신 사용하면 끝 없이 루프를 돌면서 헤더파일을 크게 만들 것이며 클래스 둘 중 하나는 제대로 컴파일이 안될 것이다.
- 하지만 헤더 파일에 꼭 import를 써야할 경우도 있다.

  1. 상속받을 때
  2. protocol를 구현해야 할 때

- Class continuation에는 Delegate같은 protocol를 선언 함으로, 헤더파일를 간편하게 할 수 있다.
- 위의 것들를 염두하여 개발하면, 문제를 만들 수 있고, 유지보수를 어렵게 하는 상호참조를 피할 수 있으며, 컴파일 타임을 줄일 수 있다.
 

3. Equivalent 방식 보다 Literal 문법 선호하기

- Literal 문법을 사용하여 소스코드의 양을 줄이고 가독성을 높여라.
- 하지만 모든 면에서 좋다고 할 수는 없다. 다음 코드를 참조하라.

  1. NSArray *arrayA = [NSArray arrayWithObjects:obj1, obj2, obj3, nil];
  2. NSArray *arrayB = @[obj1, obj2, obj3];

만약 위 코드에서 obj2, obj3가 nil이라고 하자, 그러면 arrayA는 @[obj1]가 생성이 될 것이지만 arrayB는 exception을 토해 낼 것이다.
 

4. 전처리 매크로 #define보다 타입 상수 선호하기

- 전처리 매크로인 #define은 전처리 때에 정의된 내용을 replace한다. 하지만 변수 타입이 의무화 되지 않기 때문에 각 가지 문제를 가지고 올 수 있다.
- 매크로 #define 대신에 타입형을 표시된 상수를 사용하라.
- 만약 글로벌 scope으로 상수를 선언해야 할 상황이 온다면 클래스 이름을 prefix으로 붙여서 사용해야 한다.
- 로컬 scope에서는 ‘k’를 prefix로 붙여서 네이밍을 하고, 글로벌 scope에서는 클래스 이름을 붙이는 코드 컨벤션을 지켜라.
- static과 const를 함께 사용하는 것이 중요하다. (static은 .m 파일을 오브젝트 파일로 컴파일할 때 로컬 변수의 영역을 할당하겠다는 뜻이다. const는 값을 한번 지정하고 변경하고자 할때 exception을 발생시킨다.) 만약 static을 붙이지 않으면 컴파일러는 external 지시어를 붙여 글로벌 scope으로 만들 것이다.
- NSNotification의 string 상수를 사용하고자 할때 쓰인다:

  1. //.h file
  2. extern NSString * EOCLoginManagerDidLoginNotification;
  3.  
  4. //.m file
  5. NSString *const EOCLoginManagerDidLoginNotification = @"EOCLoginManagerDidLoginNotification";

 

5. 상태, 옵션, status 코드는 enum을 사용하라

- NS_Enum, NS_Option 매크로를 사용하여 enum을 생성하자.

- switch 문을 사용하여 개발할 때 enum에 정의된 것을 빼놓고 빌드하면 경고 메세지가 뜬다.

  1. //Example of 'NS_Enum'
  2. typedef NS_ENUM(NSUInteger, EOCConnectionState){
  3.     EOCConnectionStateDisconnected,
  4.     EOCConnectionStateConnecting,
  5.     EOCConnectionStateConnected,
  6. };
  7. // is Equal To
  8. typedef enum EOCConnectionState:NSUInteger EOCConnectionState;
  9. enum EOCConnectionState:NSUInteger{
  10.     EOCConnectionStateDisconnected,
  11.     EOCConnectionStateConnecting,
  12.     EOCConnectionStateConnected,
  13. };
  14.  
  15. //Example of 'NS_Option'
  16. typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection){
  17.     EOCPermittedDirectionUp = 1 << 0,
  18.     EOCPermittedDirectionDown = 1 << 1,
  19.     EOCPermittedDirectionLeft = 1 << 2,
  20.     EOCPermittedDirectionRight = 1 << 3,
  21. };
  22. // is Equal To
  23. typedef enum EOCPermittedDirection:int EOCPermittedDirection;
  24. enum EOCPermittedDirection:int{
  25.     EOCPermittedDirectionUp = 1 << 0,
  26.     EOCPermittedDirectionDown = 1 << 1,
  27.     EOCPermittedDirectionLeft = 1 << 2,
  28.     EOCPermittedDirectionRight = 1 << 3,
  29. };

 
 

B. Object, 메세징, 그리고 런타임

 

6. Property 이해하기

- @property 지시어를 사용하면 클래스의 인스턴트 변수의 접근자를 자동으로 만들어주게 된다. (Auto-synthesis)
- Property는 object의 데이터를 은닉하는 기능을 제공한다.
- Dot syntax를 하용할 수 있게 해준다.
- 인스턴트 변수를 자동으로 생성해준다. 생성시 ‘_’를 prefix하여 만들어준다.
- 속성:

  • Atomicity: 기본값은 locking을 사용하는 atomic이다. 만약 nonatomic을 사용하면 locking을 사용하지 않는다.
  • Read/Write: [readwrite/readonly]
  • Memory-Management Semantics: [assign/strong/weak/unsafe_unretained/copy]
  • Method Names: [getter=/setter=]

- 생성자 함수 안에서 자신의 접근자 함수를 사용해서는 안된다. 서브 클래스가 접근자 함수를 변동하여 사용 할 수 있기 때문이다.
 

7. 내부에서 인스턴스 변수는 접근자 함수말고 직접 접근하라

- 기본적으로는 클래스 내부에서 인스턴스 변수를 가지고 올 때는 직접 가지고 오고, 값을 set할 때는 접근자 함수를 사용하라. 값을 가지고 올 때 접근자 함수를 사용하지 않는 것은 method dispatch를 걸치지 않고 가지고 올 수 있기 때문에 빠르기 때문이다. 그리고 값을 set할 때는 접근자 함수를 사용하면 디버깅할 때에 이점이 있으며 메모리 관리 및 KVO 매커니즘을 사용할 수 있다는 장점이 있다.
- 내부에서 직접 접근시

  • 빠르다
  • 메모리 관리 옵션과 무관하게 값을 할당할 수 있다.
  • KVO를 사용할 수 없다.

- 내부에서 접근자 함수를 사용시:

  • messaging dispatch가 일어나기 때문에 비용이 크다.
  • 메모리관리 가능하다
  • 디버깅에 용이하다.
  • KVO를 사용할 수 있다.

- 하지만, 예외도 있다. 생성자 함수에서는 접근자 함수를  사용하지 않는 것은 서브 클래스에서 setter를 덮어 쓸 수 있기 때문이다.
 

8. 객체의 Equality를 이해하라

- 커스텀 클래스를 만들 때 isEqual 함수를 만들어 사용하라. deep comparison과 shallow comparison이 있다.
 

9. 클러스터 패턴을 사용하여 구현을 숨기라

  1. //—- Base-class —-
  2. typedef NS_ENUM(NSUInteger, EOCEmployeeType){
  3.     EOCEmployeeTypeDeveloper,
  4.     EOCEmployeeTypeDesigner,
  5.     EOCEmployeeTypeFinance,
  6. }
  7.  
  8. @interface EOCEmployee: NSObject
  9. @property(copy) NSString *name;
  10. @property NSUInteger salary;
  11.  
  12. + (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
  13. - (void)doADayWork;
  14. @end
  15.  
  16. @implementation EOCEmployee
  17. + (EOCEmployee *)employeeWithType:(EOCEmployeeType)type{
  18.     switch(type){
  19.         case EOCEmployeeTypeDeveloper:
  20.         return  [EOCEmployeeDeveloper new];
  21.         break;
  22.         case EOCEmployeeTypeDesigner:
  23.         return [EOCEmployeeDesigner new];
  24.         break;
  25.         case EOCEmployeeTypeFinance:
  26.         return [EOCEmployeeFinance new];
  27.         break;
  28.     }
  29. }
  30.  
  31. - (void)doADayWork{
  32. }
  33. @end
  34.  
  35. //—- Sub-class —-
  36. @interface EOCEmployeeDeveloper: EOCEmployee
  37. @end
  38.  
  39. @implementation EOCEmployeeDeveloper
  40. - (void)doADayWork{
  41.     [self writeCode];
  42. }
  43. @end

 

10. Associated Object를 사용하여 클래스에 데이터 붙이기

- void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)를 통해서 객체에 데이터를 할당할 수 있다.
- void objc_getAssociatedObject(id object, void *key)를 통해서 담겨져 있는 데이터를 불러 올 수 있다.
- NSDictionary와 비슷한 방법으로 key를 넣어 데이터를 저장 붙일 수 있는데, 다른 점이 있다면 key는 set에 사용했던 key 포인터를 써야한다.
- objc_associatedObject는 다음과 같은 Policy를 사용하여 메모리 관리를 하게 된다.

  • OBJC_ASSOCIATION_ASSIGN: assign
  • OBJC_ASSOCIATION_RETAIN_NONATOMIC: nonatomic, retain
  • OBJC_ASSOCIATION_COPY_NONATOMIC: nonatomic, copy
  • OBJC_ASSOCIATION_RETAIN: retain
  • OBJC_ASSOCIATION_COPY: copy

- objc_associatedObject는 최후의 수단으로 사용을 해야한다. 버그가 생기면 찾기 힘든 포인트가 되기 때문이다.

  1. //Objc_association의 사용 예제
  2. // Test.m
  3.  
  4. #import <objc/runtime.h>
  5.  
  6. static void *TESTMyAlertViewKey = "TESTMyAlertViewKey";
  7.  
  8. - (void)askUserAQuestion{
  9.     UIAlertView *alert = [[UIAlertView alloc]
  10.                             initWithTitle: @"Question"
  11.                             message: @"What do you want to do?"
  12.                             delegate: self
  13.                             cancelButtonTitle:@"Cancel"
  14.                             otherButtonTitles:@"Continue", nil];
  15.  
  16.     void (^block)(NSInteger) = ^(NSInteger buttonIndex){
  17.         if(buttonIndex == 0){
  18.             [self doCancel];
  19.         }else{
  20.             [self doContinue];
  21.         }
  22.     };
  23.  
  24.     objc_setAssociatedObject(alert, TESTMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
  25.     [self show];
  26. }
  27.  
  28. - (void)alertView:(UIAlertView *)alertView
  29.         clickedButtonAtIndex:(NSInteger)buttonIndex{
  30.  
  31.     void (^block) (NSInteger) = objc_getAssociatedObject(alertView, TESTMyAlertViewKey);
  32.  
  33.     block(buttonIndex)
  34. }

 

11. objc_msgSend의 역할 이해하기

- 메세지는 다음과 같은 것을 포함 하고 있다.

  • 이름
  • selector
  • 인수(arguments)
  • 반환타입(없을 수도 있다)

- c로 만들어진 objc_msgSend 함수는 해당 객체의 메서드 테이블에 가서 해당 메서드가 있는지 확인하고 있다면, 그리로 껑충 뛰어서 코드를 진행하게 된다. 만약 찾지 못했다면 상속관계도를 따라 올라가 메서드가 있는지 확인하게 된다. 만약 그래도 없다면 message forwarding이라는 메커니즘이 끼어들게 된다.
- 함수 호출 한번에 여러 단계가 있는 것 처럼 보인다. 하지만 각 클래스 마다 캐시를 사용하여 한번 호출 된 것은 저장하였다가 사용하기 때문에 같은 객체의 같은 이름의 함수를 사용하고자 한다면 처음보다 빠르게 실행할 수 있다.

  1. id returnValue = objc_msgSend(someObject,
  2.                               @selector(messageName:),
  3.                               parameter);

 

12. Message Forwarding 이해하기

- 메세징 디스패치는 런타임에서 이루어진다. 그렇기 때문에 때로는 존재하지 않는 메서드 메세징을 시도하는 실수를 범하기도 한다. 이를 핸들링하기 위해 message forwarding 매커니즘이 존재한다.
- 핸들링하는 세가지 스탭이 있다.

  1. Dynamic method resolution: resolveInstanceMethod: 사용
  2. Replacement Receiver: – (id)forwardingTargetForSelector:(SEL)selector 사용
  3. Full Forwarding 메카니즘: – (id)forwardInvocation:(NSInvocation *)invocation 사용

- 위 핸들링을 시도해도 정의되어 있는 조치가 없으면 예외처리를 던지게 된다.

 

13. Method Swizzling을 통해 메서드 구현을 바꿔치기 하자

- 클래스의 implementation은 ‘IMP’라고 하는 함수 포인터로 저장되어 있다. 오프젝티브 씨에서는 Dynamic messaging system에서 해당 함수 포인터를 참조하여 메세지를 보내는 방식을 취하고 있다.
- 클래스마다 메서드 명들이 기술되어 있는 테이블를 조작할 수 있는 몇몇 함수들이 존재한다. 추가, 기존 implementation의 함수 포인터 가지고 오기, 바뀌치기등 몇몇 조작이 가능한데, ‘void method_exchangeImplementations(Method m1, Method m2)’가 그중의 일부이다.
- Method Swizzling은 디버깅를 위한 의도로 사용되는 것이 가장 적합하며, 가장 유용하게 사용될 수 있다.

  1. @interface NSString (EOCMyAdditions)
  2. - (NSString *)eoc_myLowercaseString;
  3. @end
  4.  
  5. @implementation NSString (EOCMyAdditions)
  6. - (NSString *)eoc_myLowercaseString{
  7.     NSString *lowercase = [self eoc_myLowercaseString];
  8.     NSLog(@"%@", lowercase);
  9.     return lowercase;
  10. }
  11. @end
  12.  
  13. Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
  14. Method swappedMethod = class_getInstanceMethod([NSString class], @selector(eoc_myLowercaseString));
  15.  
  16. method_exchangeImplementations(originalMethod, swappedMethod);

- 위 코드를 살펴 보면 재귀 호출을 하는 것 처럼 보이지만 사실은 메서드 스위즐링을 하고 나서 이 함수의 IMP는 기존의 lowercaseString를 가리키고 있다는 것을 기억하자.
 

14. Object 이해하기 (Introspection 메서드를 최대한 사용하자)

- 오브젝트의 타입은 컴파일 타임이 아닌 런타임에 결정이 된다.
- id 타입을 사용하여 컴파일 타임에 dynamic programming을 할 수 있는데, id는 컴파일 타임때 어떤 메서드들를 호출 가능하게 구현이 되어있다. 하지만 특정 메서드 호출의 시도는 런타임으로 가지고 간다.

  1. NSString *stringType = @"Some string";
  2. id genericType = @"Some string";

- 위 예제 코드에서 genericType는 의미상 NSString 포인터이다. 하지만 다는 점이 있다면 컴파일 타임에 NSString에 기술되어 있는 메서드가 있는지 없는지 알고 있다 없다의 정도의 차이이다.
- NSObject의 프로토콜에는 introspection이라고 불리는 런타임 때에 오브젝트 타입을 확인할 수 있는 메서드를 제공하고 있다.
  1. NSMutableDictionary *dict = [NSMutableDictionary new];
  2. [dict isMemberOfClass:[NSDictionary class]];//NO
  3. [dict isMemberOfClass:[NSMutableDictionary class]];//YES
  4. [dict isKindOfClass:[NSDictionary class]];//YES
  5. [dict isKindOfClass:[NSArray class]];//NO

 
 

C. API 디자인와 인터페이스

 

15. 클래스 이름 앞에 자신만의 Prefix를 붙이자

- Objective-C에서는 name space 지원을 하지 않는다.
- Open Source 라이브러리를 사용하면, dynamic loader를 통해 적재되기 때문에 개발시 이름 충돌을 모를 수 있다.
- Apple에서 두 글자 Prefix를 예약했기 때문에 세글자 prefix를 사용해야 한다.
 

16. Designated 생성자를 만들어라

- 다양한 모든 생성자는 designated 생성자를 통해 작동해야 한다.
- 부모의 designated 생성자는 꼭 사용에 맞게 override를 해서 사용해야 한다.

  1. @interface EOCRectangle: NSObject <NSCoding>
  2. @property (nonatomic, assign, readonly) float width;
  3. @property (nonatomic, assign, readonly) float height;
  4.  
  5. - (id)initWithWidth:(float)width andHeight:(float)height;
  6. @end
  7.  
  8. @implementation EOCRectangle
  9. - (id)initWithWidth:(float)width andHeight:(float)height{
  10.     if((self = [super init])){
  11.         _width = width;
  12.         _height = height;
  13.     }
  14.  
  15.     return self;
  16. }
  17.  
  18. - (id)init{
  19.     return [self initWithWidth:5.0f andHeight:10.0f];
  20. }
  21.  
  22. - (id)initWithCoder:(NSCoder *)decoder{
  23.     if((self = [super init])){
  24.         _width = [decoder decodeFloatForKey:@"width"];
  25.         _height = [decoder decodeFloatForKey:@"height"];
  26.     }
  27.     return self;
  28. }
  29. @end
  30.  
  31. //====================================================================
  32.  
  33. @interface EOCSquare : EOCRectangle
  34. - (id)initWithDimension:(float)dimension;
  35. @end
  36.  
  37. @implementation EOCSquare
  38. - (id)initWithDimension:(float)dimension{
  39.     return [super initWithWidth:dimension andHeight:dimension];
  40. }
  41.  
  42. - (id)iniWithWidth:(float)width andHeight:(float)height{
  43.     float dimension = MAX(width, height);
  44.     return [self initWithDimension:dimension];
  45. }
  46.  
  47. - (id)initWithCoder:(NSCoder *)decoder{
  48.     if((self = [super initWithCoder:decoder])){
  49.  
  50.     }
  51.     return self;
  52. }
  53. @end

 

17. description 메서드를 작성하라

- LLVM 디버깅 명령어 중 po(print object)를 사용할 때 description 메서드를 호출하기 때문에 커스텀 클래스는 description 메서드를 작성하는 것이 좋다.
 

18. Immutable Object를 선호하라

- @property의 readonly라 하여도 KVO를 통해서 변경이 가능하다. eg. [obj setValue:@"abc" forKey:@"identifier"];
- mutable 변수를 @property로 사용할 때 직접 값을 변경하게 말고, add, remove 함수를 사용해서 간접적으로 접근하도록 하라.
 

19. 명확하고 일관적인 네이밍을 사용하라

- 긴 이름의 함수명을 짓는 것을 꺼리지 마라.
- 하지만 쓸데 없이 긴 이름은 피하라.
- 네이밍 Tip (메서드)

  1. 초기화하는 팩토리 메서드의 이름은 객체 타입을 시작으로 함수명을 지어라.
  2. parameter는 명사로 지어라.
  3. 뭔가 실행하는 함수는 <동사 + 명사>, 이런식으로 이름을 지어라.
  4. 줄임말은 피하라.
  5. boolean 타입의 반환 타입의 메서드 명은 is나 has로 시작하는 이름을 지어라.
  6. get으로 시작하는 메서드 이름은 Accessor 메서드에게 양보하라.

 

20. private 메서드의 이름에 prefix를 붙여라

- public 메서드와 private 메서드를 나누어 표히하면 디버깅이 쉬어진다.
- 리팩토리시 메서드 이름이나 signature를 바꿀 때 더 쉽게 구별이 가능하다.
- 싱글 언더스코어 “_” 로 시작하는 prefix은 Apple에서 예약을 했다. 피하라.
 

21. Objective-C의 Error 처리 방식을 이해하라.

- ARC는 예외처리 안전 모드가 기본값이 아니다.
- 메모리에 할당된 resource가 @try로 인해서 release 타이밍을 놓치게 될 수도 있다.
- exception은 치명적인 에러에만 사용하라. (fatal error)
- 일반적인 에러 처리 방법

  1. delegate으로 error를 전달하라
  2. out 파라미터로 메서드에 전달하라. eg. – (BOOL)doSomething:(NSError **)error

 

22. NSCopying 프로토콜 이해하기

- -(id)copyWithZone:(NSZone *)zone을 사용하여 객체를 복사하라.
- 최근 방식은 zone은 하나만 사용되어 지고 있다.
- copy 메서드를 오버라이드 하지마라. 대신 copyWithZone:을 구현하라.
- deep copy / shallow copy를 구분하라.
 
 

D. 프로토컬과 카테고리

 

23. Object 내부간의 통신은 delegate이나 Data source 프로토콜을 이용하라

- delegate 패턴은 데이터를 비즈니스 로직으로 부터 분리해준다.
- delegate은 @property의 weak으로 선언하여서 non-owning 관계를 유지하는 것이 중요하다.
 

24. Category를 사용하여 방대한 클래스를 나누어라

 

25. 항상 서드 파티 라이브러리의 Category는 prefix을 붙여 사용하라

- Category는 마지막으로 추가된 것이 최종으로 override하게 된다. 그러므로 prefix을 하용하여 혼란을 방지해야한다. 그래서 메서드 이름 앞에 prefix를 붙이고 category 이름도 다르게 하는 것이 좋다.
 

26. Category에서는 @property를 피하라

- 모든 property는 메인 클래스에서 선언하여 사용하라.
- category는 functionality 확장으로만 봐야지, 데이터 확장은 피하는 것이 좋다.
- 때로는 readonly property는 category에 넣어도 좋다.
 

27. Class-Continuation을 통해 구현을 감추어라

 

28. Protocol을 통해 무명의 객체를 제공하라

- eg. @property id<BTBCompletionHandler> delegate;
 
 

E. 메모리 관리

 

29. Reference Counting 이해하기

 

30. ARC를 사용하라

 

31. Reference를 release하고 Observation을 해지하는 것은 dealloc에서만 하라

- dealloc에서 NSNotification, KVO등을 해지하라.
- OS 자원인 file description이나 DB관련 객체도 dealloc에서 해재하라.
- dealloc에서 메서드 호출을 피하라.
 

32. Exception 안전 코드와 함께 메모리 관리하기

- (수동 레퍼런스 카운트 관리) try문 안에 retain하는 객체는 코드 진행중 exception 발생으로 release 할 타이밍을 놓칠 경우가 생길 가능성이 있다. Finally 구문으로 객체 해제를 해야한다.
- ARC에서 기본 값으로 finally 구문에 retain 된 객체를 해지하지 않는다. -fobj-arc-exception 컴파일러 플래그로 컴파일 타임에 release코드를 포함시켜 주자.
 

 33. Retain cycle을 피하기 위해 Weak reference를 사용하라

- 만약 own하지 않는 것은 retain하지 말라.
 

34. Autorelease Pool Block 사용하기

- 반드시 꼭 자신만의 auto release pool를 만들어서 사용해야 하는 것은 아니다.
- @autoreleasepool 문법을 사용하여 Loop 구문에 들어 가있는 tmp 변수들을 정리해주자.
 

35. Zombie 기능을 사용하여 메모리 관리를 하자

- Zombie 객체를 알아보는 디버깅 툴을 사용하여 retain되어 release되지 않는 코드를 알아보고 최적해보자.
 

36. retainCount 메서드 사용을 피하자

 
 

F. 블럭과 GCD 이해하기

 

37. Block 이해하기

- 블록은 다른 값과 맞찬가지로 자신만의 타입을 가지고 있다. Primitive 타입 변수나 Reference 타입 변수과 같이 변수와 연계할 수 있고 다른 변수 처럼 사용될 수 있다.
- 문법: return_type (^block-Name) parameters
- 블럭 밖에서 선언된 변수를 블럭 안에서 수정하고자 한다면 ‘__block’수식어를 통해 표시해야 한다.
- 블럭도 reference 카운팅을 하여 memory 관리에 속하게 된다.
- 블럭 안에서 instance 변수를 ‘__block’ 수식어 없이 사용가능 한데, self와 함께 capture된다.
 

38. 자주 사용하는 블럭 타입을 typedefine을 사용하여 선언하자

- #typedefine은 다른 타입의 alias를 제공한다.

eg. typedef int(^EOBlockName)(BOOL flag, in val) //선언

EOBlockName block = ^(BOOL flag, int val){….} // 사용
 

39. 코드 분리를 줄이기 위해 핸드러 block을 사용하라

- delegate 패턴으로 처리하여 코드가 여러 곳으로 분리되는 것은 직관적이지 못하다. 그리고 또한 delegate 함수를 여러 경우에 사용할 경우 구분해줘야 하는 코드가 추가적으로 구현되어 있어야 한다. 이럴때 블럭을 사용하여 직관적이고 표현력을 높히는 방법을 사용하는 것이 좋다.
- 블럭 처리의 두 가지 종류:

  1. startWithCompletion:failure:
  2. startWithCompletion: (하나의 블럭안에 ^(data, error))

 

40. Block이 객체를 소유하고 있어 생기는 retain cycle를 피하라

 

41. dispatch queue를 사용하여 스레드 lock을 실행하라

- GCD 출연 전 통상적으로 사용하던 방식

  1. @synchronized(self)를 통해 critical section 구현하기
  2. [NSLock() lock] / [NSLock() unlock] 메서드 직접 사용하기

- concurrent queue와 barrier_block이 추천하는 조합이다.
 

42. performSelector보다 GCD를 선호하라

 

43. GCD와 Operation Queue를 구분하여 사용하라

- GCD 강점:

  1. GCD는 C library이다.
  2. synchronization (critical section)을 구현할 경우 좋다.
  3. dispatch_once를 통해 코드 한번만 실행하기
  4. lightweight한 작업만 사용할 것을 추천한다.

- NSOperation 장점:

  1. cancelling operations
  2. operation dependencies
  3. KVO를 적용된 Operation property를 사용할 때: NSOperation은 각 단계에 있어 flag를 제공한다. 이때 KVO를 사용해 flag의 변화에 귀를 기울이자.
  4. 우선순위: GCD에서는 Queue 자체에 우선순위를 줄 수 있지만, 각 Task에 우선순위를 줄 수 없다.
  5. 재사용성 용이

 

44. Dispatch_Group을 사용하라

 

45. Thread safe한 단한번의 실행을 위해 dispatch_Once를 사용하라

- token은 반드시 static이나 global scope의 변수여야 한다.

46. dispatch_get_current_queue의 사용을 피하라

- 현재 deprecated 되었다. 디버깅을 위해서만 사용하라.
- dispatch queue는 내부적으로 hierarchy 구조로 설계되어 있으므로 단순히 해당 메서드를 사용하여 현재 사용중인 큐를 알아내는 것은 무의미 하다.
 
 

G. 시스템 프래임워크

 

47. System 프래임워크와 친해져라

- toll-free bridging은 CoreFoundation와 Foundation 프래임워크를 이어주는 역할을 한다.
 

48. Loop 대신 enumeration을 선호하라

- 통상적으로 사용하는 loop 방식은 extra 변수를 할당해야 하며 처리해야한다. 대신 enumeration 메서드를 사용하면 보다 쉽고 메모리 관리에 용이한 방법을 사용할 수 있다.
- enumerateObjectsUsingBlock:
- enumerateKeyAndObjectsUsingBlock:
 

49. Collection 타입의 자원의 메모리 관리를 위해 Toll-Free Bridging을 사용하라

- Toll-free bridging 은 foundation에서 정의한 objective-C 클래스와 CoreFoundation에 정의한 C 데이터 구조를 형볌환해주는 중요한 역할을 해준다.
 

50. NSDictionary 대신 캐시 역할을 해주는 NSCache를 사용하라

- NSCache는 memory가 꽉차면 자동으로 저장되어 있는 데이터를 release해준다. (사용한지 오래된 데이터 순서로)
- NSCache는 copy하지 않고 retain한다.
- Thread safe하다.
- NSCache 대신 NSPurgeableData를 사용해도 된다.
 

51. initialize 함수와 load 함수 사용을 선호하라

- load 메서드 사용은 디버깅 목적으로만 사용하는 것이 좋다.
- initialize는 클래스가 사용되기 전 단 한번 불려진다.
 

52. NSTimer는 자신의 타겟을 retain한다는 것을 잊지 마라

 

[Obj-C] 개체 복사하기 – NSCopying -

Objective-C에서 클래스 인스턴스를 복사할 때 구현 되어 있어야 하는 Protocol은 NSCopying이다. 해당 Protocol은 하나의 인터페이스를 제공한다.

  1. - (id)copyWithZone:(NSZone *)zone

여기에서 Zone은 메모리에 있는 데이터의 Segment들이 있는 구역을 뜻한다. 옛날 그 어느날에는 memory segment를 특정 영역에 생성하여 관래했다고 한다. 하지만 꽤 오래전 부터 사용하지 않는 기술이다. 지금은 모든 앱은 하나의 Zone을 가지고 있다. 그러므로 인수 값인 zone에 대해서 크게 신경 쓰지 말고 기본적으로 무엇인지에 대해서만 인지하고 있으면 될 것이다.

보통 개발을 하다가 복사를 하고 싶으면 NSObject에 있는 copy 메서드를 사용하거나 mutableCopy를 사용하여 mutable 개체를 복사하고 있을 것이다. 사용자가 정의한 개체 복사를 위해는 NSObject의 copy나 mutableCopy 메서드를 override해서 사용하면 안된다. Copy 메서드가 호출하는 copyWithZone: 메서드를 override해서 사용해야 한다.

  1. #import <Foundation/Foundation.h>
  2.  
  3. @interface EOCPerson : NSObject <NSCopying>
  4.  
  5. @property (nonatomic, copy, readonly) NSString *firstName;
  6. @property (nonatomic, copy, readonly) NSString *lastName;
  7.  
  8. - (id)initWithFirstName:(NSString*)firstName
  9.             andLastName:(NSString*)lastName;
  10.  
  11. - (void)addFriend:(EOCPerson*)person;
  12. - (void)removeFriend:(EOCPerson*)person;
  13.  
  14. @end
  15.  
  16. @implementation EOCPerson {
  17.     NSMutableSet *_friends;
  18. }
  19.  
  20. - (id)initWithFirstName:(NSString*)firstName
  21.             andLastName:(NSString*)lastName {
  22.     if ((self = [super init])) {
  23.         _firstName = [firstName copy];
  24.         _lastName = [lastName copy];
  25.         _friends = [NSMutableSet new];
  26.     }
  27.     return self;
  28. }
  29.  
  30. - (void)addFriend:(EOCPerson*)person {
  31.     [_friends addObject:person];
  32. }
  33.  
  34. - (void)removeFriend:(EOCPerson*)person {
  35.     [_friends removeObject:person];
  36. }
  37.  
  38. - (id)copyWithZone:(NSZone*)zone {
  39.     EOCPerson *copy = [[[self class] allocWithZone:zone]
  40.                        initWithFirstName:_firstName
  41.                              andLastName:_lastName];
  42.     copy->_friends = [_friends mutableCopy];
  43.     return copy;
  44. }
  45.  
  46. @end

위 코드에서 copyWithZone: 메서드의 copy->_friends를 주목하자. _friends는 internal 인스턴스이기 때문에 -> 문법을 사용했음을 기억하자.

복사 개념에는 ‘Shallow Copy’(얕은 복사)와 ‘Deep Copy’(깊은 복사)가 있다. 얕은 복사란 인스턴스 프로퍼티로 Collection 타입의 개체가 있을 때 컨테이너는 복사를 하되 속의 있는 내용물은 기존 컨테이너가 가지고 있던 레퍼런스를 포인트하고 있는 개념이다. 깊은 복사는 말 그대로 클론을 하나 만들어서 새로운 메모리 영역에 올리는 것을 뜻한다.  NSCopying는 기본적으로 얕은 복사를 한다. 그러므로 깊은 복사를 하기 위해서는 추가적인 조취가 필요하다.

Effective Objective-C 2.0 Matt Galloway

Effective Objective-C 2.0 Matt Galloway

위 사진을 보면 얕은 복사는 컨테이너는 복사가 되었지만 내용물은 원본 개체의 내용물을 가르키고 있는 것을 볼 수있다. 반면 깊은 복사는 클론을 만들어 낸 것을 볼수 있다.

  1. - (id)deepCopy {
  2.     EOCPerson *copy = [[[self class] alloc]
  3.                        initWithFirstName:_firstName
  4.                              andLastName:_lastName];
  5.     copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
  6.                                              copyItems:YES];
  7.     return copy;
  8. }

NSMutableSet의 initWithSet:copyItems: 생성자를 통해서 깊은 복사를 할 수 있다.

Objective-C는 무슨 Endian인가?!

시작하며..

개인적으로 Bit Manipulation에 대해 공부를 하다가 궁금한 부분이여서 얇은 구글링을 통해 Endian에 대해 알아보게 되었는데, 스스로는 잊어 버리지 않기위해 남김이며, 더 나아가 혹시 궁금해서 검색하는 분들에게도 도움이 되지 않을까 하는 약간의 기대감을 갖고 글을 적어 본다.

Endian이란 무엇인가?

현재 컴퓨터 설계 측면에서 보면 Big-Endian을 사용하는 컴퓨터가 있고, Little-Endian을 사용하는 컴퓨터가 있다. Unix와 Linux 프로세서에서는 Big-Endian을 사용하고 있고, Inter x32, x64 계열에서 Little-Endian을 사용하고 있다고 한다. 그렇다면 여기에서 말하는 Endian은 과연 무엇인가?

Endian은 메모리에 byte를 읽고 쓰는 순서에 대한 방식을 말한다. 32 bit 컴퓨터는 4 byte 단위로 메모리에 데이터를 읽고 쓰는 동작을 하는데, 이때 높은 byte 부터 쓰느냐 혹은 낮은 것 부터 쓰느냐 여부는 자신의 컴퓨터 CPU 설계 방식에 따라 달라지게 된다. 예를 들면, 빅 엔디안 컴퓨터에서는 16진수 “4F52″를 저장공간에 “4F52″라고 저장할 것이다 (만약 4F가 1000번지에 저장되었다면, 52는 1001번지에 저장될 것이다). 반면에, 리틀 엔디안 시스템에서 이것은 “524F”와 같이 저장될 것이다.(출처)

왜 중요한가?

그렇다면 왜 이렇게 byte를 쓰는 순서가 중요한 것일까? 그 이유는 서로 다른 컴퓨터가 채택한 Endian이 다르기 때문이다. 만약 혼자 프로그램을 짜고 자신의 컴터에서만 사용한다면 큰 문제가 없겠지만, 네트워크 통신을 해서 보낸 데이터의 Endian 방식과 받는 컴퓨터의 Endian 방식이 상이 하다면 뜻하지 않은 결과가 나오기 때문이다. 그렇기 때문에 사용하게 될 프로토콜에서 정의하는 Endian이 무엇인지 확인한 후 프로그래밍을 해야 하며, 때에 따라 Little-Endian을 Big-Endian으로 변경하는 코드를 넣어야 할 것이다.

증명 코드

그렇다면 iOS 개발자로써 궁금해지는 것이 있다. Objective-C은 과연 어떤 Endian을 기본적으로 사용하고 있는지 궁금할 것이다. 답을 먼저 말한다면 당연히 ‘Little-Endian’이다. 개인적인 생각은 Intel CPU를 사용하고 있으니 당연히 그럴 것이라고 생각을 했지만, 이를 증명하기 위한 코드를 간단히 짜보면 안다.

  1. #include
  2.  
  3. int main() {
  4.     long x = 0×44434241;//"DCBA"
  5.     char *y = (char *) &x;
  6.  
  7.     if(strncmp(y,"ABCD",4)){
  8.         printf("Big Endian\n");
  9.     }else{
  10.         printf("little Endian\n");
  11.     }
  12. }

StackOver Flow에서 발취함.
코드는 strncmp 함수를 사용하여 “DCBA” 문자열과 “ABCD”문자열을 비교하는 간단한 실험이다. 만약 Big-Endian로 저장이 된 것이라면 x 변수가 저장 될때 0×44, 0×43, 0×42, 0×41 순서되로 저장 될 것이다. (0×41는 “A”이다.) 하지만 Little-Endian으로 저장이 된다면 이 순서가 반대로 저장이 될 것이다. 코드를 돌려보면 알겠지만 console창에는 ‘little Endian’이 찍힌다.

마치며…

실 프로그래밍 생활에서 기본이 되기에 살포시(?!) 무시하고 지나갈 수 있는 내용이지만 remind하는 차원에서 읽었으면 좋겠다. 그래도 이해가 안간다면 링크에 자세한 설명이 나와 있으니 한번 읽어보면 쉽게 이해가 갈것이라 생각되어 진다.

[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

[Build Setting::Other Linker Flags] -ObjC와 -all_load의 차이점

Other Linker Flag‘는 XCode 프로젝트에서 서드 파티 라이브러리를 사용할 때 흔히 세팅하는 build setting이다. 해당 플래그는 컴파일 타임 때에 서드 파티의 링크하는 과정에 대한 설정인데, 일반적으로 가장 많이 사용되는 Flag는 ‘-ObjC’와 ‘-all_load’ 이다. 언듯 보면 똑같은 역할을 하는 플래그 처럼 보이지만 다음과 같은 차이가 있다고 한다.

-Objc

This flag causes the linker to load every object file in the library that defines an Objective-C class or category.

해당 값은 링커가 Objective-C 클래스나 카테고리로 정의된 라이브러리에 속한 객체 파일들을 모두 적재하게 해주는 플래그이다.

-all_load

For 64-bit and iPhone OS applications, there is a linker bug that prevents -ObjC from loading objects files from static libraries that contain only categories and no classes. The workaround is to use the -all_load or -force_load flags. -all_load forces the linker to load all object files from every archive it sees, even those without Objective-C code

64 비트 프로젝트나 아이폰 OS 응용 프로그램에서 클래스가 없고 카테고리만 있는 Static 라이브러리의 객체를 -ObjC만 사용하여 적재하려고 할 때 정상 작동을 하지 않는 버그가 있다. 그 버그를 위해 대처 안으로 사용할 수 있는 것이 ‘-all_load‘나 ‘-force_load‘  플래그이다. ‘-all_load‘ 플래그는 모든 객체 파일을 링커로 부터 적재할 수 있도록 하는 플래그이다.

Reference Building Objective-C static libraries with categories

from: http://stackoverflow.com/questions/21844824/difference-between-all-load-and-objc

NSOperation VS Grand Central Dispatch

NSOperation vs. Grand Central Dispatch (GCD)

You may have heard of Grand Central Dispatch (GCD). In a nutshell, GCD consists of language features, runtime libraries, and system enhancements to provide systemic and comprehensive improvements to support concurrency on multi-core hardware in iOS and OS X. If you’d like to learn more about GCD, you can read our Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.

GCD라는 것을 들어봤을 것이다. 간략하게 설명하자면, GCD는 언어 기능과 런타임 라이브러리, 그리고 iOS와 OS X이 작동하는 멀티-코어 하드웨어 위에서 동시성을 지원해주는 시스템 강화 기능등으로 구성되어 있다. 만약 GCD에 대해 더 자세한 것을 알길 원한다면 블로그 ‘Multithreading and Grand Central Dispatch on iOS for Beginners Tutorial.’를 읽어보길 권유한다.

Before Mac OS X v10.6 and iOS 4, NSOperation and NSOperationQueue were different from GCD and used two completely different mechanisms. Starting with Mac OS X v10.6 and iOS 4, NSOperation and NSOperationQueue were built on top of GCD. As a very general rule, Apple recommends using highest-level abstraction, and then dropping down to lower-levels when measurements show they are needed.

OS X 10.6과 iOS 4 이전 버전에서는 NSOperation과 GCD의 메카니즘은 전혀 달랐다. 하지만 Mac OS X 10.6과 iOS 4 버전에서 부터 NSOperation과 NSOperationQueue는 GCD 위에서 설계되었다. 보통 애플은 개발자들에게 상위 레벨에 추상화 된 레이어를 먼저 사용하길 권유하고 있다. 그리고 나서 필요에 따라 낮은 레벨의 추상화된 기능들을 쓰길 권고한다. 

Here’s a quick comparison of the two that will help you decide when and where to use GCD or NSOperation and NSOperationQueue:

그럼 GCD를 써야 할지 혹은 NSOperation과 NSOperationQueue를 써야 할지 고민하는 당신을 돕기 위한 비교한 내용을 살펴보자.

  • GCD is a lightweight way to represent units of work that are going to be executed concurrently. You don’t schedule these units of work; the system takes care of scheduling for you. Adding dependency among blocks can be a headache. Canceling or suspending a block creates extra work for you as a developer! :]
  • GCD는 동시에 실행하려는 작업 단위를 대표 할 수있는 경량의 방법이다. 해당 작업 단위는 개발자가 직접 스케줄하지 않고 시스템이 스케줄 관리를 해준다. 블럭들 사이에서 의존성을 부여하는 것은 쉽지 않은 일이며, 작업 취소 혹은 일시정시 같은 일을 하기 위해서는 각 개발자가 개인별로 추가해야한다.
  • NSOperation and NSOperationQueue add a little extra overhead compared to GCD, but you can add dependency among various operations. You can re-use operations, cancel or suspend them. NSOperation is compatible with Key-Value Observation (KVO); for example, you can have an NSOperation start running by listening to NSNotificationCenter.
  • NSOperation과 NSOperationQueue는 GCD에 비해 추가적인 기능을 제공하며 여러 operation에 의존성을 부여할 수도 있다. 뿐만 아니라 재사용도 가능하며 취소 혹은 일시정지와 같은 기능도 가능하다. NSOperation은 KVO 기술을 완벽하게 사용할 수 있다. 그래서 NSOperation이 실행되기 시작하면 NSNotificationCenter를 통해 상태 변화에 대한 노티를 받을 수 있다. 

from http://www.raywenderlich.com/19788/how-to-use-nsoperations-and-nsoperationqueues

Objective-C 빌드에 관한 기본 개념 정리

Objective-C 프로젝트를 빌드하기 위해서 기본적으로 알아 둬야할 개념들이 있다. 많은 초보 개발자들이 놓치고 있는 부분이기도 한다. 프로젝트를 생성하고 기본으로 설정되어 있는 세팅을 가지고 빌드를 하게 되면 무난하게 잘 작동을 하기 때문에 아마 큰 관심이 없었을 것이다. 하지만 프래임워크를 만들려고 한다던지, 혹은 서드 파티의 라이브러리를 빌드한 다음 자신의 코드를 빌드하는 등 세팅을 많이 건드려야 한다면 반드시 알고 지나가야하는 개념들이다.

이번 블로그에서 설명할 개념들은 아래와 같다.

  • Workspace
  • Project
  • Scheme
  • Target
  • Archive

 

#Workspace

Workspace는 하나 이상의 Project를 포함 할 수 있는 컨테이너의 개념이다. 서로 dependency가 있는 프로젝트 끼리 한 작업 환경에 모아 놓으면 개발이 용이하고, 편리하다. 프로젝트 서로 간에 자신이 가지고 있는 Target들을 연관시켜 작업할 수도 있기 때문에 실개발에서 꽤 많이 사용되는 매커니즘이다.

#Project

프로젝트는 소스 파일를 참조하여 개발하는 Workspace 다음으로 높은 레이어이다. 프로젝트는 Scheme과 Target 그리고 Build 설정을 포함하고 있다. 뿐만 아니라 UI를 제작할 수 있는 스토리 보드와 같은 파일도 프로젝트에서 생성 관리가 가능하다.

#Scheme

scheme은 특정한 Target을 빌드하기 위한 스팩이라 할 수있다. 다섯 가지의 scheme 액션을 행할 수 있는데, 실행, 테스트, profile을 통한 성능 실험, 코드 분석, 아카이브와 배포이다. (단 빌드는 이 모든 액션을 실행하기 위해서 먼저 실행되어야 한다.)  Scheme을 통해 Target에 액션을 실행 할 수 있다.

#Target

Target은 한 마디로 설명하자면, 다음과 같다. ‘제품을 빌드한다.’ 엄밀히 말하자면 특정한 product을 만들 기 위한 지시서이다. 소스 코드의 부분 혹은 전체를 참조 할 수 있으며, Build Phase라는 상위 레벨의 빌드하는 순서를 세팅할 수 있다. 또한 각 Target 마다 Build Setting을 통해 어떻게 빌드 할 것인지를 결정 할 수 있다. 뿐만 아니라 다른 Target와도 의존 관계를 맺고 빌드 할 수도 있다.

#Archive

아카이브는 단 하나의 앱만 담고 있다. 그렇기 때문에 해당 앱에서는 라이브러리와 프래임워크를 포함하고 있다. 뿐만 아니라 타임 스템프가 찍혀 있는 번들로써 다음과 같은 정보를 포함하고 있다.

  • 당신의 앱 빌드를 설치할 스타일
  • 디버그 Symbols DSYM 파일로 부터 분리
  • 앱 스토어에 올리기 위한 확인 작업에 대한 메타 데이터와 서브 미션에 관한 데이터
  • 아카이브에 달린 comments

 

OBJC_ASSOCIATION에 대하여.

프로젝트를 하다보면 코드가 복잡해지고 클래스의 길이가 늘어가는 경우가 있다. 여러 CASE에서 미흡한 설계와 리팩토링 실력으로 발생되는 산출물이라고 생각된다. 그 중에서도 한 클래스에서 한번 이상의 UIAlertViewDelegate이나, UIActionSheetDelegate을 사용할 때 어디에서 이벤트가 발생되었는지 식별해주기 위한 코드로 인해 지저분하고 너저분한 코드를 만들어 내곤 했다. (물론 Delegate Protocol를 다른 클래스로 분리한다면 언급한 문제는 없어질 수 있으나, 분명 한 코드의 Handler를 다른 클래스에 옮겨 가면서 보는 것은 가독성을 떨어뜨리는 코드임은 분명하다.) (뭐 객체지향 프로그래밍의 당연한 모습이라고 주장하는 사람이 있을지 모르겠지만…)

Objective-C 2.0 라이브러리 중에 ‘objc/runtime.h’이라는 라이브러리가 있다. 그 안에 여러가지 기능을 담고 있겠지만, 오늘 살펴 볼 기능은 런타임 환경에서 기존 객체에 타 객체를 연관(associated)시킬 수 있는 기능에 대해서 살펴 보고자 한다. 먼저 자세한 설명을 하기 전에 예제 코드를 통해 한번 살펴 보는 것이 빠를 것으로 생각된다.

예제 코드

  1. #import "ViewController.h"
  2.  
  3. //#1.
  4. #import <objc/runtime.h>
  5.  
  6. #pragma mark – Global Variables
  7.  
  8. typedef NS_ENUM(NSUInteger, AlertMeBttonTag)
  9. {
  10.     AlertMeBttonTagOne,
  11.     AlertMeBttonTagTwo
  12. };
  13.  
  14. //#2.
  15. static void *AlertViewKey = "AlertViewKey";
  16.  
  17. #pragma mark – Interface extension of 'ViewController'
  18.  
  19. @interface ViewController ()<UIAlertViewDelegate>
  20. @end
  21.  
  22. #pragma mark – Implementation of 'ViewController'
  23.  
  24. @implementation ViewController
  25.  
  26. - (void)viewDidLoad
  27. {
  28.     [super viewDidLoad];
  29. }
  30.  
  31. - (void)didReceiveMemoryWarning
  32. {
  33.     [super didReceiveMemoryWarning];
  34. }
  35.  
  36. #pragma mark – IBActions
  37.  
  38. - (IBAction)tappedAlertmeButton:(id)sender
  39. {
  40.     UIButton *tappedButton = (UIButton *)sender;
  41.     
  42.     if (tappedButton.tag == AlertMeBttonTagOne)
  43.         [self showAlertViewOne];
  44.     if (tappedButton.tag == AlertMeBttonTagTwo)
  45.         [self showAlertViewTwo];
  46. }
  47.  
  48. #pragma mark – Delegate Protocol of 'UIAlertViewDelegate'
  49.  
  50. - (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
  51. {
  52.     //#3.
  53.     void (^block)(NSInteger) = objc_getAssociatedObject(alertView, AlertViewKey);
  54.     
  55.     block(buttonIndex);
  56. }
  57.  
  58. #pragma mark – Methods
  59.  
  60. - (void)showAlertViewOne
  61. {
  62.     UIAlertView *alertviewOne = nil;
  63.     alertviewOne = [[UIAlertView alloc] initWithTitle:@"Boom Alert View ONE!"
  64.                                               message:@"ONE!"
  65.                                              delegate:self
  66.                                     cancelButtonTitle:@"Close"
  67.                                     otherButtonTitles:@"OK", nil];
  68.     
  69.     void (^AlertViewOneCloseBlock)(NSInteger) = ^(NSInteger buttonIndex)
  70.     {
  71.         NSLog(@"Alert view # ONE: %d", buttonIndex);
  72.     };
  73.     
  74.     //#4.
  75.     objc_setAssociatedObject(alertviewOne,
  76.                              AlertViewKey,
  77.                              AlertViewOneCloseBlock,
  78.                              OBJC_ASSOCIATION_COPY);
  79.     
  80.     [alertviewOne show];
  81. }
  82.  
  83. - (void)showAlertViewTwo
  84. {
  85.     UIAlertView *alertviewTwo = nil;
  86.     alertviewTwo = [[UIAlertView alloc] initWithTitle:@"Boom Alert View TWO!"
  87.                                               message:@"TWO!"
  88.                                              delegate:self
  89.                                     cancelButtonTitle:@"Close"
  90.                                     otherButtonTitles:@"OK", nil];
  91.     
  92.     void (^AlertViewTwoCloseBlock)(NSInteger) = ^(NSInteger buttonIndex)
  93.     {
  94.         NSLog(@"Alert view # TWO: %d", buttonIndex);
  95.     };
  96.     
  97.     //#5.
  98.     objc_setAssociatedObject(alertviewTwo,
  99.                              AlertViewKey,
  100.                              AlertViewTwoCloseBlock,
  101.                              OBJC_ASSOCIATION_COPY);
  102.     
  103.     [alertviewTwo show];
  104. }
  105.  
  106. @end

코드 설명

이 코드는 ViewController에 IBOutlet UIButton 타입의 멤버 변수가 두 개 있고, 탭을 하면 AlertView를 팝업 해주는 단순한 프로그램 코드이다. 이때, alertview가 닫힐 때 UIAlertViewDelegate의 프로토콜을 사용하여 다음 단계의 코드 진행을 유도하는 형식으로 구성해보았다. 두 개가 다른 메서드에서 두개가 다른 UIAlertView 타입의 객체를 통해 Show 메세징을 보내고 alertView가 닫히면 protocol로 그 다음 코드를 진행하고 있다. 그런데 만약 objc_association 코드를 사용하지 않고 두 개의 close 이벤트에 대한 핸들링을 하기 위해서는 여러가지가 있겠지만, 직관적이고 깔끔한 코드를 짜기 위해서 많은 고민을 했을 것이다. 각 핸들러에 대한 파일을 따로 가지고 간다든지, 아니 delegate 메서드에서 해당 관련있는 UIAlertView를 식별해 낼 수 있는 코드를 추가하는 등 번거로움이 따를 것이다. 하지만 NSNotificationCenter나 KVO와 같이 키를 등록하고 불러오는 식의 방식으로 한 객체를 타 객체에 붙일 수 있는 기능을 ‘objc/runtime.h’에서 제공한다. 그럼 코드를 살펴보고 사용법과 주의 점에 대해 알아보자.

  1. 해당 기능을 사용하기 위해서 ‘objc/runtime.h’를 임포트 시켜준다.
  2. objc association을 사용하기 위한 키를 선언한다.
  3. objc_getAssociatedObject를 통해 등록한 커스텀 데이터를 불러온다.
  4. objc_setAssociatedObject를 통해 한 객체에 커스텀 데이터를 연관시키는 코드이다.

사용방법

  • void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) 객체의 연관을 설정하기 위해서는 붙이고하는 값을 정의된 키와 메모리 관리 정책과 함께 사용한다.
  • id objc_getAssociatedObject(id object, void *key) 데이터를 첨부한 객체와 키를 가지고 불러온다.
  • void objc_removeAssociatedObjects(id object)는 등록 된 데이터를 삭제하는데 사용된다.

일단 ‘객체 연관’을 사용할 때 필요한 키에 대해서 말해보자. 키는 NSNotification이나 KVO와 같은데서 사용되는 NSString 객체가 isEqualToString: 메서드로 구분되는 키가 아니라 등록할 때 가르키고 있는 실제 객체의 포인터가 필요하다. 위 예제에서 보면 2번이 바로 그 역할을 하는 static 전역 변수이다. 그리고 커스텀 데이터를 한 객체에 붙이고자 할 때에, 메모리 retain cycle이 발생하여 메모리 누수를 발생 시킬 수 있다. 그렇기 때문에 사용되는 여부에 따라서 Policy를 잘 선택하여 객체를 등록시켜야 한다. 메모리 정책은 다음과 같다.

OBJC_ASSOCIATION_POLICY

주의사항

‘객체 연관’은 편리한 기능을 제공하지만 단점이 있다. 디버깅에 어려움이 있다는 것이다. 개인적인 생각은 Unit TEST 코드를 잘 짜놨더라면 큰 어려움은 없어 보이지만 실제 해당 부분에서 버그가 생기면 디버깅하기가 여간 까다로운 것이 아니라고 한다. 그리고 메모리 누수를 조심해야 할 것이다.

마무리

2년 넘게 Objective-C를 사용하면서 오늘도 새삼 깨달는 것은 ‘아직도 공부할 부분이 많다!’라는 것이다. 자신이 있는 그곳에 머물며 자리를 지키려고 하기 보다는 앞으로 전진하려는 노력을 통해 한 분야에 자타공인하는 ‘장인’이 되고 싶은 것이 꿈이다. 그날까지 화이팅이다.

[Objective-C 문법] 블럭 (Block)

요즘 Concurrent programming을 드려보다가 자주 쓰는데 쓰려고 하면 생각이 안나는 이상한(?!) 문법하나 적어두려고 이렇게 포스팅하게 되었다.

애플에서는 C 함수와 생김새가 비슷한 블럭 객체를 NS계열의 라이브러리에 많이 사용하고 있는 것을 볼 수 있다. 개인적으로는 Delegate을 쓸까 Block을 쓸까 고민을 많이 하는 편인데, 이제는 어느정도 머리에 확립이 되었다. 재사용해야하는 경우에는 클래스 기반의 Delegate을 사용고, 한번 사용할 로직이라면 보통 블럭을 사용하고 있다.

혹시 이 글을 읽으시는 분 중에서 더 좋은 경우가 있다면 댓글을 달아주시면 고맙겠다.

암튼. 그럼 블럭인 무엇인지 살펴보자.

Block (블럭)

O’REILY에서 나온 ‘iOS 7 Programming Cookbook’의 저자는 블럭을 다음과 같이 정의했다.

“Block objects are package of code that usually appear in the form of methods in Object-C.” (블럭 객체는 보통 Objective-C에서 메서드 모습을 한 코드 패키지이다.)

만약 누군가 개인적으로 블럭을 정의하라고 한다면 나는 아마 이렇게 대답할 것이다.

“블럭은 로직을 객체화해주는 또 다른 하나의 매커니즘이다.”

물론 Objective-C 프로그래밍은 Foundation에서 지원해주는 NSInvocator라는 클래스를 제공하고 있다. 하지만 보다 간편하게 사용할 수 있기 때문에 사용 빈도가 점점 늘어나고 있다.

어떻게 생겼나?

그럼 이제부터 본격적으로 문법에 대해서 알아보자. 다음은 Objective-C 스타일의 메서드와 C 함수 스타일, 그리고 블럭 스타일의 코드를 보여주고 있다.

 

  1. //#1. Objective-C Style
  2. - (void)CanCallThisBlock:(NSString *)strVal
  3. {
  4.     NSLog(@"strVal: %@", strVal);
  5. }
  6.  
  7. //#2. C style
  8. void CanCallThisBlock(NSString *strVal)
  9. {
  10.     NSLog(@"strVal: %@", strVal);
  11. }
  12.  
  13. //#3. Block
  14. void (^CanCallThisBlock)(NSString *) = ^(NSString *strVal)
  15. {
  16.     NSLog(@"strVal: %@", strVal);
  17. };

 

그리고 자주 사용하는 문법 중 하나는 typedef를 사용하여 컴파일러에게 블럭의 시그니쳐를 알려주는 코드는 다음과 같다.

  1. typedef void(^CanCallThisBlock)(NSString *);

위 코드의 내용은 클래스 내에서 인터페이스 확장자(Interface extension: ‘@interface ViewController()’) 내에 선언된 메서드의 선언문과 동일한 역할을 한다.

특징

블럭 밖에서 선언된 변수는 블럭 안으로 들어오면 ‘__block’ 예약어가 없는 한 read_only 속성으로 copy가 된다. 다음 코드를 살펴보자.

  1. NSUInteger intVal = 10;
  2.  
  3. CopyTest block = ^
  4. {
  5.     NSLog(@"#1. intVal = %d", intVal);
  6. };
  7.  
  8. intVal = 20;
  9.  
  10. //Test Final Result
  11. block();
  12. NSLog(@"#2. intVal = %d", intVal);
  13.  
  14. //결과:
  15. //#1. intVal = 10
  16. //#2. intVal = 20

위 코드를 보는 것 같이 intVal 변수는 ‘__block’이라는 예약어 없이 선언이 되어 있다. 그렇기 때문에 값이 변경 전의 값인 10가 그대로 남아있는 것이다.

그 밖에…

iOS 7이 나오면서 Objective-C 문법 자체에도 더 강화가 되어서 업데이트가 되었다. 사실 iOS 6를 사용해서 개발할 때만 해도 블럭 안에서 self를 사용할 수가 없었다. 그렇기 때문에 사용하기 위해서는 __block으로 선언한 변수를 하나 만들어서 사용을 하던지, 블럭을 정의 할 때 파라미터로 self를 받아서 사용을 했었다. 그런데 최신 버전에는 그런 걱정 없이 정상 작동을 하고 있다.

블럭을 변수에 할당 해서 사용을 하든지 inline(인라인)으로 사용을 하든지 개인적으로 가장 중요하다고 여기는 것은 가독성(Readability)인 것 같다. 블럭을 사용하는 순간, 화면에 뭔가 모를 걸리적 거림이 있는 것은 나 혼자 뿐일지 모르겠지만, 분명 블럭을 사용함에 있어서 깔끔한 느낌을 덜 주게 되는 것은 사실이다. 적당히 알맞은 장소에서 사용을 한다면 정말 강력한 무기가 될 것이고 그렇지 않고 모든 부분에서 블럭을 사용한다면 디버깅 뿐만 아니라 코드를 유지보수하는 측면에서 어려움을 맛 볼 것으로 여겨진다.

Updated December.19.2013

TDD로 개발 중 블럭을 Unit Test하기 위해 리서치를 하다가 못보던 블럭 문법을 발견하여 업데이트 하게 되었다. 블럭은 code에 inline 되어지기 때문에 여간 테스트하기 어려운 존재였는데, 좋은 방법을 찾았다. 다음과 같이 블럭을 메서드화 시켜서 선언해 사용하도록 하자.

  1. //Header 파일에는 다음과 같이 선언한다.
  2. - (void(^)(NSString *))instanceBlock;
  3.  
  4. // .m 파일에는 다음과 같이 정의한다.
  5. - (void(^)(NSString *))instanceBlock
  6. {
  7.     return ^(NSString *str)
  8.     {
  9.         NSLog(@"Output: %@", str);
  10.     };
  11. }
  12.  
  13. //그리고 Test 파일에서 사용할 때는 다음과 같이 사용한다.
  14. [myInstance instanceBlock](@"helloBarty!");

이 기법을 배운 포스트이다. 재사용 가능할 뿐만 아니라 테스트 가능하며, 그리고 가독성도 높여주는 좋은 방법이다.