프로젝트를 하다보면 코드가 복잡해지고 클래스의 길이가 늘어가는 경우가 있다. 여러 CASE에서 미흡한 설계와 리팩토링 실력으로 발생되는 산출물이라고 생각된다. 그 중에서도 한 클래스에서 한번 이상의 UIAlertViewDelegate이나, UIActionSheetDelegate을 사용할 때 어디에서 이벤트가 발생되었는지 식별해주기 위한 코드로 인해 지저분하고 너저분한 코드를 만들어 내곤 했다. (물론 Delegate Protocol를 다른 클래스로 분리한다면 언급한 문제는 없어질 수 있으나, 분명 한 코드의 Handler를 다른 클래스에 옮겨 가면서 보는 것은 가독성을 떨어뜨리는 코드임은 분명하다.) (뭐 객체지향 프로그래밍의 당연한 모습이라고 주장하는 사람이 있을지 모르겠지만…)
Objective-C 2.0 라이브러리 중에 ‘objc/runtime.h’이라는 라이브러리가 있다. 그 안에 여러가지 기능을 담고 있겠지만, 오늘 살펴 볼 기능은 런타임 환경에서 기존 객체에 타 객체를 연관(associated)시킬 수 있는 기능에 대해서 살펴 보고자 한다. 먼저 자세한 설명을 하기 전에 예제 코드를 통해 한번 살펴 보는 것이 빠를 것으로 생각된다.
예제 코드
#import "ViewController.h"
//#1.
#import <objc/runtime.h>
#pragma mark – Global Variables
typedef NS_ENUM(NSUInteger, AlertMeBttonTag)
{
AlertMeBttonTagOne,
AlertMeBttonTagTwo
};
//#2.
static void *AlertViewKey = "AlertViewKey";
#pragma mark – Interface extension of 'ViewController'
@interface ViewController ()<UIAlertViewDelegate>
@end
#pragma mark – Implementation of 'ViewController'
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
#pragma mark – IBActions
- (IBAction)tappedAlertmeButton:(id)sender
{
UIButton *tappedButton = (UIButton *)sender;
if (tappedButton.tag == AlertMeBttonTagOne)
[self showAlertViewOne];
if (tappedButton.tag == AlertMeBttonTagTwo)
[self showAlertViewTwo];
}
#pragma mark – Delegate Protocol of 'UIAlertViewDelegate'
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
//#3.
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, AlertViewKey);
block(buttonIndex);
}
#pragma mark – Methods
- (void)showAlertViewOne
{
UIAlertView *alertviewOne = nil;
alertviewOne = [[UIAlertView alloc] initWithTitle:@"Boom Alert View ONE!"
message:@"ONE!"
delegate:self
cancelButtonTitle:@"Close"
otherButtonTitles:@"OK", nil];
void (^AlertViewOneCloseBlock)(NSInteger) = ^(NSInteger buttonIndex)
{
NSLog(@"Alert view # ONE: %d", buttonIndex);
};
//#4.
objc_setAssociatedObject(alertviewOne,
AlertViewKey,
AlertViewOneCloseBlock,
OBJC_ASSOCIATION_COPY);
[alertviewOne show];
}
- (void)showAlertViewTwo
{
UIAlertView *alertviewTwo = nil;
alertviewTwo = [[UIAlertView alloc] initWithTitle:@"Boom Alert View TWO!"
message:@"TWO!"
delegate:self
cancelButtonTitle:@"Close"
otherButtonTitles:@"OK", nil];
void (^AlertViewTwoCloseBlock)(NSInteger) = ^(NSInteger buttonIndex)
{
NSLog(@"Alert view # TWO: %d", buttonIndex);
};
//#5.
objc_setAssociatedObject(alertviewTwo,
AlertViewKey,
AlertViewTwoCloseBlock,
OBJC_ASSOCIATION_COPY);
[alertviewTwo show];
}
@end
#import "ViewController.h"
//#1.
#import <objc/runtime.h>
#pragma mark – Global Variables
typedef NS_ENUM(NSUInteger, AlertMeBttonTag)
{
AlertMeBttonTagOne,
AlertMeBttonTagTwo
};
//#2.
static void *AlertViewKey = "AlertViewKey";
#pragma mark – Interface extension of 'ViewController'
@interface ViewController ()<UIAlertViewDelegate>
@end
#pragma mark – Implementation of 'ViewController'
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
}
#pragma mark – IBActions
- (IBAction)tappedAlertmeButton:(id)sender
{
UIButton *tappedButton = (UIButton *)sender;
if (tappedButton.tag == AlertMeBttonTagOne)
[self showAlertViewOne];
if (tappedButton.tag == AlertMeBttonTagTwo)
[self showAlertViewTwo];
}
#pragma mark – Delegate Protocol of 'UIAlertViewDelegate'
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
//#3.
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, AlertViewKey);
block(buttonIndex);
}
#pragma mark – Methods
- (void)showAlertViewOne
{
UIAlertView *alertviewOne = nil;
alertviewOne = [[UIAlertView alloc] initWithTitle:@"Boom Alert View ONE!"
message:@"ONE!"
delegate:self
cancelButtonTitle:@"Close"
otherButtonTitles:@"OK", nil];
void (^AlertViewOneCloseBlock)(NSInteger) = ^(NSInteger buttonIndex)
{
NSLog(@"Alert view # ONE: %d", buttonIndex);
};
//#4.
objc_setAssociatedObject(alertviewOne,
AlertViewKey,
AlertViewOneCloseBlock,
OBJC_ASSOCIATION_COPY);
[alertviewOne show];
}
- (void)showAlertViewTwo
{
UIAlertView *alertviewTwo = nil;
alertviewTwo = [[UIAlertView alloc] initWithTitle:@"Boom Alert View TWO!"
message:@"TWO!"
delegate:self
cancelButtonTitle:@"Close"
otherButtonTitles:@"OK", nil];
void (^AlertViewTwoCloseBlock)(NSInteger) = ^(NSInteger buttonIndex)
{
NSLog(@"Alert view # TWO: %d", buttonIndex);
};
//#5.
objc_setAssociatedObject(alertviewTwo,
AlertViewKey,
AlertViewTwoCloseBlock,
OBJC_ASSOCIATION_COPY);
[alertviewTwo show];
}
@end
코드 설명
이 코드는 ViewController에 IBOutlet UIButton 타입의 멤버 변수가 두 개 있고, 탭을 하면 AlertView를 팝업 해주는 단순한 프로그램 코드이다. 이때, alertview가 닫힐 때 UIAlertViewDelegate의 프로토콜을 사용하여 다음 단계의 코드 진행을 유도하는 형식으로 구성해보았다. 두 개가 다른 메서드에서 두개가 다른 UIAlertView 타입의 객체를 통해 Show 메세징을 보내고 alertView가 닫히면 protocol로 그 다음 코드를 진행하고 있다. 그런데 만약 objc_association 코드를 사용하지 않고 두 개의 close 이벤트에 대한 핸들링을 하기 위해서는 여러가지가 있겠지만, 직관적이고 깔끔한 코드를 짜기 위해서 많은 고민을 했을 것이다. 각 핸들러에 대한 파일을 따로 가지고 간다든지, 아니 delegate 메서드에서 해당 관련있는 UIAlertView를 식별해 낼 수 있는 코드를 추가하는 등 번거로움이 따를 것이다. 하지만 NSNotificationCenter나 KVO와 같이 키를 등록하고 불러오는 식의 방식으로 한 객체를 타 객체에 붙일 수 있는 기능을 ‘objc/runtime.h’에서 제공한다. 그럼 코드를 살펴보고 사용법과 주의 점에 대해 알아보자.
- 해당 기능을 사용하기 위해서 ‘objc/runtime.h’를 임포트 시켜준다.
- objc association을 사용하기 위한 키를 선언한다.
- objc_getAssociatedObject를 통해 등록한 커스텀 데이터를 불러온다.
- 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를 잘 선택하여 객체를 등록시켜야 한다. 메모리 정책은 다음과 같다.

주의사항
‘객체 연관’은 편리한 기능을 제공하지만 단점이 있다. 디버깅에 어려움이 있다는 것이다. 개인적인 생각은 Unit TEST 코드를 잘 짜놨더라면 큰 어려움은 없어 보이지만 실제 해당 부분에서 버그가 생기면 디버깅하기가 여간 까다로운 것이 아니라고 한다. 그리고 메모리 누수를 조심해야 할 것이다.
마무리
2년 넘게 Objective-C를 사용하면서 오늘도 새삼 깨달는 것은 ‘아직도 공부할 부분이 많다!’라는 것이다. 자신이 있는 그곳에 머물며 자리를 지키려고 하기 보다는 앞으로 전진하려는 노력을 통해 한 분야에 자타공인하는 ‘장인’이 되고 싶은 것이 꿈이다. 그날까지 화이팅이다.