2015년 9월 8일 화요일

Objective-C 제너릭(Generics)

언제 어떻게 왜 추가되었는지 몰랐던(?) Objective-C 제너릭을 간단히 정리해 보는 글이다.

Generics?!

Swift부터 공부했거나 C++등등 기타 언어를 통해 제너릭을 공부했다면 제너릭이 뭔지 감이 올 것이다. 그냥 타입에 자유로운 클래스 디자인을 가능하게 하는 개념이라고 하면 이해가 되려나...

일반적인 예를 보자. NSArray나 NSMutableArray는 기본적으로 NSObject를 상속받는 모든 값이 들어갈 수 있지만, 제너릭을 이용하면 타입을 명확하게 지정 할 수 있다.
NSMutableArray<NSString *> *strings = [[NSMutableArray alloc] init];
[strings addObject:@"Valid String"];
위의 경우 꺽쇠 안에 NSString * 라는 타입을 명시해서 strings 라는 배열을 문자열만 가질 수 있는 배열로 선언하고 있다. 이게 바로 제너릭이다.

이렇게 제너릭을 이용 할 때 얻는 이점은 역시나 컴파일 타임 때 타입 체크를 명확하게 할 수 있다는 점이다.
// 아래 코드는 Incompatible pointer types 경고를 발생시킨다.
[strings addObject:@1];
덕분에 잘못된 데이터가 들어갈 가능성을 미연에 방지 할 수 있다.

Xcode에서는 친절하게 자동완성 시 제너릭 이용 여부에 따라 자동완성 폼도 다르게 표시해 준다.


위의 경우 제너릭을 이용해 문자열 배열을 선언했더니 이 배열의 addObject 메소드 타입도 자동으로 NSString 형식으로 오도록 표시해준다.

NSDictionary나 NSMutableDictionary 처럼 두 가지 타입(키의 타입과 값의 타입)을 사용해야 하는 경우는 콤마(,)로 여러개의 타입을 지정 할 수 있다.
NSMutableDictionary<NSString *, NSNumber *> *someDictionary = [[NSMutableDictionary alloc] init];
[someDictionary setObject:@1 forKey:@"one"];
순서대로 키(key)의 타입과 값(object value)의 타입이다. 이 경우라면 키는 문자열이 되고 값은 숫자(NSNumber)가 된다.

사전형 타입의 경우 현재는 버그가 있는지 Xcode(7 Beta 6)에서는 자동완성 시 아래와 같이 표시해 주고 있다.


위 스크린샷은 키의 타입을 명확하게 표시 못 해 주고 있다. 나중에 정식으로 나오면 바뀔지도 모르겠지만... -_-;;

Objective-C Lightweight Generics

가벼운 제너릭이란건 걍 위의 제너릭 개념에 대한 이름으로 생각하자. 어쨌든, 자신만의 제너릭을 지원하는 클래스를 디자인 하려면 어떻게 해야 하는가를 살펴보자.

방법은 의외로 단순한데, 인터페이스(Interface)에 역시나 꺽쇠를 이용해 ObjectType 이라는 키워드를 붙여주면 된다.
@interface SomeClass<ObjectType> : NSObject
@property (nonatomic, strong) ObjectType value;
@end

@implementation SomeClass
@end
참고: ObjectType 이라는 이름은 애플의 권장사항일 뿐 필수는 아닐꺼라 생각된다. 실제로 아무 이름을 통일시켜서 써도 된다.
구현부(implementation)가 비어있어서 좀 심심한 클래스지만 기본적인 설명은 되는 것 같다. 하여간 이제 ObjectType 이라는 타입을 다룰 수 있게 된다.
SomeClass<NSString *> *object = [[SomeClass alloc] init];
object.value = @"Test String";

// 아래 코드는 Incompatible pointer types 경고를 발생시킨다.
object.value = @123;
생각보다 단순하게 사용 할 수 있어서 가벼운(lightweight)가보다. -_-;;
애플의 코드 예제에는 __covariant 같은 이상한 키워드가 붙는데 이게 딱히 없어도 잘 되는것 같아서 뭔지는 찾아보지 않았다. ;;;;

제너릭, 스위프트(Swift)와 친해지는 방법

한 프로젝트에서 스위프트 코드와 Objective-C 코드를 함께 쓰는건 어렵지도 않고 흔한(?) 일일거라 생각된다. 하지만 이 둘을 섞어쓰는 과정에서 안타까운 점이 있다. 아래 예를 보자.
@property (nonatomic, strong) NSArray *names;
특정 클래스의 프로퍼티 선언이다. 별 문제는 없어보인다.

하지만 이 코드의 인터페이스를 스위프트화 시키면 아래와 같다.
var names: [AnyObject]!
names 라는 이름의 프로퍼티인 만큼 원래 의도는 문자열 배열이다. 하지만 스위프트에서 바라본 인터페이스는 그저 아무런 오브젝트 배열의 암시적 옵셔널(!)로 보일 뿐이다.

이걸 제너릭과 함께 Objective-C Nullablility 를 이용해 좀더 스위프트 친화적으로 고칠 수 있다.
@property (nonatomic, strong, nonnull) NSArray<NSString *> *names;
이렇게 고치면 이제 스위프트 측에서 이 인터페이스를 바라보면 아래와 같은 식으로 인식 할 수 있다.
var names: [String]
명확하다! 스위프트의 모토인 안전한 타입(Type Safe)에도 잘 어울린다.


Xcode 에서 보면 이렇게 자동완성도 깔끔하게 볼 수 있다.

타입의 한정

아무리 타입에 자유로운 제너릭이라 할지라도 해당 타입에 특정한 기능이 없다면 동작이 불가능할 수도 있다. 예를 들자면 타입이 문자열 변환을 반드시 지원해야 한다면 등등 말이다.

아래 예제는 단순하게 타입이 특정 타입일 경우만 가능한 이상한(?) 제너릭 선언 인터페이스 예제이다.
@interface SomeClass<ObjectType : NSString *> : NSObject
@property (nonatomic, strong) ObjectType value;
@end
이제 이 SomeClass 클래스는 NSString * 타입만을 사용할 수 있는 바보같은 제너릭이 되었다. -_-;;

뭐 사실 '한정'이라는 예를 들려다 보니 이런 극단적인 예가 나오긴 했는데, 아마도 아래와 같은 형식이 더 유용한 예제같다.
@interface SomeClass<ObjectType : id<NSCoding>> : NSObject
@property (nonatomic, strong) ObjectType value;
@end
프로토콜이 사용되는 아무 포인터라는건 분명 약속을 정의하는데 좋은 요소이니 말이다. :-)

[관련글] Swift - 제너릭(Generics)
[관련글] 스위프트(Swift) 가이드

댓글 없음 :