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가 실행될 것이다.

 

마치며

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

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