카테고리는 런타임 시 이미 존재하는 클래스에 메서드를 추가할 수 있게 해준다. 어떤 클래스라도, 심지어 애플에서 제공하는 코코아의 클래스 조차도, 카테고리에 의해 확장될 수 있다. 그리고 그 새 메서드는 클래스의 모든 인스턴스에서 사용할 수 있게 된다. 이 것은 스몰토크에서 상속 받은 것으로 어떤 면에서는 C#의 메서드 확장과 유사하다.
카테코리는 커다란 클래스를 관리하기 쉬운 작은 코드 조각으로 분리하기 위해 디자인 되었고 이름은 여기에서 유래되었다. 파운데이션의 클래스들을 살펴 보면, 몇몇 조각으로 나누어져 있음을 발견하게 될 것이다. 예를 들어, NSArray는 NSArray.h에 정의된 NSExtendedArray, NSArrayCreation, 그리고 NSDeprecated 카테고리를 포함한다. 더하여 NSArrayPathExtensions 카테고리는 NSPathUtilities.h에 정의되어 있다. 대부분의 카테고리는 여러 파일에 나누어져 있다. 그러나 NSString의 UIStringDrawing 카테고리와 같은 몇몇 카테고리는 런타임 시 특정한 다른 코드를 로드한다. Mac의 AppKit은 NSStringDrawing 카테고리를 로드하고 iOS는UIStringDrawing 카테고리를 로드한다. 이 것이 #ifdef로 코드를 사용하여 코드를 분리하는 것보다 좀더 우아한 방법이다. 각 플랫폼에 따라 적절한 구현 파일(.m)이 컴파일되어 그 기능을 사용할 수 있다.
Objective-C 2.0 이전에 @protocol 정의는 선택적(optional)인 메서드를 포함할 수 없었다. 대신 개발자는 “비공식 프로토콜(inform protocol)”로 카테고리를 사용했었다. 컴파일러는 카테고리에 있는 메서드 정의를 안다. 그러나 메서드가 구현되지 않은 경우에 경고를 생성할 수 없을 것이다. 이를 이용해 프로토콜의 선택적 메서드를 만들 수 있다. 이 것에 대해서는 좀 더 자세한 것은 다른 포스트인 “공식, 비공식 프로토콜”에서 설명할 예정이다. 그러나 iOS는 카테고리를 사용하는 이 방법을 추천하지 않는다. 공식 프로토콜은 이제 선택적 메서드를 직접 지원하기 때문이다.
컴파일러가 카테고리의 메서드를 구현했는지 확인하지 않기 때문에 카테고리를 오로지 커다란 클래스를 분리하기 위해 사용하는 것은 그에 상응하는 장단점이 있다. 구현 파일의 규모가 커졌을 때 카테고리를 정의하여 분리하는 것 보다 클래스에 촛점을 두어 리팩토링이 필요한 암시라고 보는 것이 좋다. 그러나 클래스가 정확한 범위안에 있다면, 카테고리로 분리하는 것이 더 유용하다. 다른 한편, 카테고리를 사용하면 메서드가 서로 다른 파일에 분산되어 혼란스러워 진다. 그래서 현명하게 판단해야 한다.
카테고리의 정의는 간단하다. 카테고리의 이름이 괄호안에 있다는 점 외에는 클래스의 인터페이스 정의와 비슷하다.
@interface NSMutableString (Capitalize)
- (void)capitalize;
@end
위에서 예로든 카테고리의 이름은 Capitalize이다. 이 곳에 인스턴스 변수의 선언이 없음에 주목하자. 카테고리는 인스턴스 변수를 선언할 수 없다. 또한 프라퍼티를 사용할 수도 없다. 이 글의 후반에 카테고리에 데이터를 저장하는 방법에 대해 설명할 것이다.
Capitalize 카테고리는capitalize 실제로 어떤 곳에 구현되었는지 상관하지 않는다. 만약 구현되지 않았고 호출되면 예외를 발생할 것이다. 컴파일러는 이 곳에서 안전을 보장하지 않는다. 만약 구현했다면, 관습에 따라 다음과 같을 것이다.
@implementation NSMutableString (Capitalize)
- (void)capitalize {
[self setString:[self capitalizedString]];
}
@end
“관습에 따라”라고 한 이유는 카테고리 구현 시 정의 되어야 하거나 또는 카테고리 구현이 카테고리 인터페이스와 같은 이름이어야 한다는 요구사항이 없기 때문이다. 그러므로 @implementation의 블럭 이름을 Capitalize라고 하면,
반드시 @interface 블럭 네임이 Capitalize에 선언된 모든 메서드를 반드시 구현해야 한다. 다른 파일(.m)에 메서드를 추가할 클래스 이름 다음에 괄호로 시작하여 카테고리 이름을 추가한다. 카테고리 메서드를 원래 클래스의 implementation 블럭 안과 카테고리 이름을 정의한 implementation 블럭 안에 구현할 수 있다. 또는 전혀 구현 하지 않을 수도 있다.
기술적으로 카테고리는 메서드 오버라이가 가능하다. 그러나 위험하므로 권장하지 않는다. 만약 두 개의 카테고리가 같은 메서드를 구현하면 어떤 것이 사용되는 지 확실하지 않게 된다. 나중에 관리의 이유로 클래스가 카테고리로 분리되면 확실하지 않은 것이 오버라이드될 수 있다. 이런 종류의 버그는 추적하기 매우 힘들다. 게다가 이 기능을 사용하면 코드를 이해하기 어렵게 만든다. 카테고리 오버라이드는 또한 원래의 메서드를 호출할 방법을 제공하지 않는다. 디버깅을 제외하고는 존재하는 메서드를 오버라이드하기 위해 카테고리를 사용하지 말기를 권한다. 심지어 디버깅일지라도... 차라리 스위즐링(swizzling)를 사용하길 권한다.
기존의 클래스에 유티리티 메서드를 추가하기 위해 카테고리를 사용하는 것은 좋은 예이다. 이렇게 할 때, 헤더와 구현 파일에 기존 클래스의 이름에 확장 이름을 사용할 것을 권장한다. 예를 들어 NSDate에 MyExtensions 카테고리를 생성할 수 있다.
@interface NSDate (MYExtensions)
- (NSTimeInterval)timeIntervalUntilNow;
@end
@implementation NSDate (MYExtensions)
- (NSTimeInterval)timeIntervalUntilNow {
return [self timeIntervalSinceNow];
}
@end
오직 몇 개의 유티릴티 메서드만 있다면 모두를 MYExtensions(각자의 코드에서 사용하는 접두어를 사용면 된다)과 같은 이름으로 하나의 카테고리에 넣는 것이 유용하다. 이렇게 하면 각 프로젝트를 확장하기가 더 쉽다. 물론 이 방식은 코드가 팽창하는 단점이 있다. 그래서 “유틸리티” 카테고리에 얼마나 포함시킬 지에 주의해야 한다. Objective-C는 C 또는 C++과 같이 불필요한 코드를 효과적으로 제거할 수 없다.
만약 연관된 메서드의 커다란 그룹이 있다면 특히 그런 것이 항상 유용하지는 않지만, 각 그룹별로 카테고리로 나누는 것은 좋은 생각이다. UIKit에 있는 UIStringDrawing.h는 좋은 예이다.
+load
카테고리는 런타임에 클래스에 첨부된다. 카테고리가 정의되어 있는 라이브러리는 동적으로 로드된다. 그래서 카테고리는 추가 시 약간 지체된다. (동적 라이브러리를 직접 iOS에 추가할 수 없는 반면, 시스템 프레임워크는 동적으로 로드되고 카테고리를 포함한다. Objective-C는 카테고리가 처음 첨부되면 실행할 수 있는 +load를 제공한다. +initialize와 유사하게, 이 것을 정적 변수를 초기화하는 것과 마찬가지로 카테고리의 특정 설정을 구현하기 위해 사용할 수 있다. 클래스가 이미 구현하고 있기 때문에 +initialize를 카테고리에 안전하게 사용할 수는 없다. 만약 여러 카테고리에서+initialize를 구현한다면, 어떤 것이 실행될지 확실치 않다.
다음과 같은 질문을 하게 될 것이다. “만약 카테고리가 다른 카테고리와의 충돌 때문에 +initialize를 사용할 수 없다면, 여러 카테고리에서 +load은 어떻게 구현할까?” 이 것이 Objective-C 런타임의 마법같은 기능 중의 하나로 그 진가를 알 수 있다. +load 메서드는 런타임에서 특별한 경우이다. 그래서 모든 카테고리는 그 것을 구현한다. 그리고 구현된 모든 것이 실행될 것이다. 단, 순서는 보장되지 않는다. 그리고 직접 +load를 호출해서는 안된다.
+load는 카테고리가 정적인지 동적인지 상관 없이 호출된다. 카테고리가 런타임에 추가되면 호출된다. 보통 프로그램 시작 시, main 이전, 그러나 훨씬 나중에.
클래스 마다 각각 +load 메서드(카테고리에 정의된 것은 아니다)를 소유한다. 그리고 클래스가 런타임 시 추가되면 호출 된다. 동적으로 클래스를 추가하지 않는한 그리 유용하지는 않다.
+initialize가 그러하듯이 +load가 여러 번 실행되지 않도록 할 필요는 없다. +load 메시지는 실제 구현된 클래스에게 오직 한 번 전송된다. 그래서+initialize에서 사용했던 방식대로 서브클래스에서 우현히 호출하지 않아야 한다. 모든 +load는 정확히 한 번 호출된다. [super load]를 호출하지는 말라.
associative reference를 이용한 카테고리 데이터 저장
카테고리는 새로운 인스턴스 변수를 생성할 수 없다. Associative References를 사용할 수는 있다. Associative References를 사용하여 임의의 객체에 키-밸류 데이터를 추가할 수 있다.
Person 클래스의 경우를 생각해 보자. emailAddress라는 새로운 프라퍼티를 추가하기 위해 카테고리를 사용하는 것을 더 좋아하게 될 것이다. 아마도 다른 프로그램에서 Person을 사용할 것이고 때론 이메일 주소는 사용할 수도 안할 수도 있다. 그래서 카테고리라는 이메일 주소가 필요치 않을 경우에 오버헤드를 피할 수 있는 좋은 해결책을 사용할 수 있다. 또는 Person 클래스를 소유하지 않을 수도 있다. 그리고 클래스 소유자가 프라퍼티를 추가할 권한을 주지 않는다. 이 경우 이 문제를 어떻게 해결할 수 있을까? 우선 Person 클래스를 살펴 보자.
@interface Person : NSObject
@property (readwrite, copy) NSString *name;
@end
@implementation Person
@synthesize name = name_;
@end
이제associative reference를 사용하여 카테고리에emailAddress이라는 새로운 프라퍼티를 추가할 수 있다.
#import <objc/runtime.h>
#import "Person.h"
@interface Person (EmailAddress)
@property (readwrite, copy) NSString *emailAddress;
@end
#import "Person+EmailAddress.h"
@implementation Person (EmailAddress)
static char emailAddressKey;
- (NSString *)emailAddress {
return objc_getAssociatedObject(self, &emailAddressKey);
}
- (void)setEmailAddress:(NSString *)emailAddress {
objc_setAssociatedObject(self, &emailAddressKey, emailAddress, OBJC_ASSOCIATION_COPY);
}
@end
associative reference는 값이 아닌 키의 주소를 기반으로 한다는 점에 주의하자. emailAddress에 어떤 값이 저장되어도 상관 없다. 오직 고유의 주소가 필요하다. 그렇기 때문에unassigned static char룰 키로 사용했다.
associative reference는 메모리 관리에 잇점을 갖는다. 파라미터에 objective_setAssociatedObject를 전달하여 정확하게 copy, assign, 또는 retain 의미를 조정한다. 연관된 객체가 해제되면 정확히 릴리즈를 한다.
associative reference는 얼럿 창이나 컨트롤에 연관된 객체를 추가하는 좋은 방법이다. 예를 들어, 다음의 코드에서 처럼 “represented object”를 얼럿 창에 추가할 수 있다.
- (IBAction)doSomething:(id)sender {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Alert"
message:nil
delegate:self
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
objc_setAssociatedObject(alert, &kRepresentedObject, sender, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[alert show];
}
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
UIButton *sender = objc_getAssociatedObject(alertView, &kRepresentedObject);
self.buttonLabel.text = [[sender titleLabel] text];
}
많은 프로그램은 currentAlertObject와 같은 호출자에서 인스턴스 변수를 사용한다. 그러나 associative reference는 간결하고 단순하다. 맥 개발에 익숙한 사람에게는 representedObject와 유사할 것이다. 그러나 더 유연하다.
associative reference(또는 카테고리에 데이터를 추가하는 다른 접근 방식)의 한 가지 제약 사항은 encodeWithCoder:와 통합할 수 없다는 것이다. 그래서 카테고리를 직렬화하기가 어렵다.
플라이웨이트 패턴을 이용한 카테고리 데이터 저장
만약 Mac 10.6 이전 버전과 코드를 공유해야 한다면 associative reference를 사용할 수 없고 플라이웨이트 패턴(flyweight pattern)이라는 예전 방법을 사용해야 한다. 객체 내부에 데이터를 저장하는 대신 객체 밖에 저장한다. 그리고 그 데이터를 사용하기 위해 키를 관리한다. 이 경우 각 Person 인스턴스는 반드시 고유의 idenfifier가 필요하다. 다음 예를 보자.
@interface Person : NSObject
@property (readonly, copy) NSString *identifier;
@property (readwrite, copy) NSString *name;
- (Person *)initWithIdentifier:(NSString *)anIdentifier;
@end
@implementation Person
@synthesize identifier = identifier_;
@synthesize name = name_;
- (Person *)initWithIdentifier:(NSString *)anIdentifier {
if ((self = [super init])) {
identifier_ = [anIdentifier copy];
}
return self;
}
@end
이제 emailAddress 데이터를 저장하기 위해 정적
NSMutableDictionary를 생성한다.
@interface Person (EmailAddress)
@property (readwrite, copy) NSString *emailAddress;
@end
@implementation Person (EmailAddress)
static NSMutableDictionary *sEmailAddressForIdentifier = nil;
+ (void)load {
sEmailAddressForIdentifier = [[NSMutableDictionary alloc] init];
}
- (NSString *)emailAddress {
return [sEmailAddressForIdentifier objectForKey:[self identifier]];
}
- (void)setEmailAddress:(NSString *)anAddress {
[sEmailAddressForIdentifier setObject:[anAddress copy]
forKey:[self identifier]];
}
@end
이제 다음과 같이Persion 객체를 위한 이메일 주소를 저장할 수 있다.
Person *person = [[Person alloc] initWithIdentifier:@"park"];
person.name = @"Lambert";
person.emailAddress = @"LambertPark@gmail.com";
이 방식은 몇 가지 문제를 안고 있다. Person 객체가 삭제될 때 어떻게든 추적하지 않는한, 메모리를 릴리즈할 좋은 방법이 없다. 문제의 범위가 광범위 하긴 하지만, 이 방식은 associative references를 선택할 수 없는 경우에 매우 잘 작동한다.
클래스 확장
Objective-C 2.0은 카테고리를 비틀은 클래스 확장(class extension)이라는 유용한 기능을 추가했다. 카테고리의 이름을 제외하고는 카테고리와 매우 비슷하다.
@interface Person ()
- (void)doSomething;
@end
클래스 확장은 구현 파일(.m)에 프라이빗 메서드를 추가하는 아주 좋은 방법이다. 카테고리와 확장이 다른 점은 확장에서 선언한 메서드들은 인터페이스에서 선언한 메서드와 정확히 일치한다는 점이다. 컴파일러는 구현된 모든 것을 확인할 것이다. 그리고 카테고리와 같이 런타임이 아닌 컴파일 시점에 추가할 것이다. 확장에 씬서사이즈된 프라퍼티 역시 추가할 수 있다.