2017년 1월 6일 금요일

Objective-C 코드의 Swift 별명 이야기 (Swift 3 기준)

편의성 측면도 있겠지만 애플에서 Swift 를 강력하게 밀고 있기에 아무래도 macOS나 iOS용 개발 언어는 Swift 가 주류가 될 가능성이 높다. 하지만 그렇더라도 한동안은 Objective-C 와의 동거를 끝낼 수는 없을 것이다. 그 증거로 애플에서는 Xcode 나 Swift 가 업데이트 될 때 마다 Objective-C 코드와의 공존에 대한 기능을 계속 발전시키고 있다.

이번 글은 Objective-C 코드가 Swift 에서 어떻게 읽혀지는지 혹은 Swift 용 별명을 지어서 언어간 차이를 완화시키는 방법이 있는지를 간단히(?) 적어볼까 한다.

우선 Swift 프로젝트에서 Objective-C 코드를 사용하게 되는 경우로 한정한다. 이 경우 대부분의 Objective-C 코드는 브릿지 헤더(Bridging Header)에 명시된 헤더파일에서 알려주는 인터페이스는 거의 그대로 Swift 에서 이용이 가능하다. 이 중 주요한 몇 가지만 골라보자.

참고로 이 글은 좀 낡았으며 Swift에 어울리는 Objective-C 코드 작성하기 글에서 좀 더 최신의 내용을 다루고 있다.

클래스(Class)

클래스의 경우 대부분의 메소드들은 거의 1:1로 Swift 에서 활용이 가능하다. 문법적으로 비슷한 부분이 있기에 가능하기도 하거니와 Xcode 가 자체적으로 번역(?)해 주는 기능들도 있기 때문이다. 특히 Swift 3 (Xcode 8)에 들어서는 Objective-C 코드들을 아예 Swift 와 비슷한 형식의 별칭을 만들어줘서 위화감을 덜 느끼게 해 준다.

대표적인 예로 아래 코드를 보자. Objective-C 코드의 헤더로써 특정 클래스의 인터페이스를 정의하고 있다.
@interface MyGoodClass : NSObject

@property (nonatomic, copy) NSString *name;

+ (MyGoodClass *)goodClassWithName:(NSString *)name;        // 1
+ (MyGoodClass *)goodClassForUserName:(NSString *)userName; // 2

// 3
+ (MyGoodClass *)basicObjectWithName:(NSString *)name;

// 4
+ (MyGoodClass *)anotherObjectWithName:(NSString *)name NS_SWIFT_NAME(init(anotherObjectName:));

@end
프로퍼티는 거의 동일하니 생략하고, 네 개의 정적(static) 메소드가 있다. 모두 자기 자신의 타입 오브젝트를 리턴하는 생성자와 비슷한 역활의 메소드들이다. 이 중 주석으로 1, 2로 표기한 녀석들의 경우 Swift 에서는 아래처럼 사용이 가능하다.
let goodObject1 = MyGoodClass(name: "test")
let goodObject2 = MyGoodClass(forUserName: "another user")
보다싶이 Swift 스럽게 번역이 되어서 위화감 없이 쓸 수 있다.

하지만 3번의 경우는 좀 다르다.
let basicObject = MyGoodClass.basicObject(withName: "basic user")
이 경우는 Swift 스럽지 않게 그냥 직설적으로 Objective-C 코드를 그대로 가져다 쓴다는 느낌이 든다.

여기까지만 보면 어느 정도 추측이 들 것이다. 클래스 이름과 동일한 혹은 비슷한 이름의 메소드를 쓰면 Swift 의 생성자(init) 형태로 번역해 준다는 점을 말이다. 반대로, 이름이 매치가 안될 정도로 다르면 번역을 하지 않고 그대로 Swift 인터페이스를 만들어준다.

만약 Swift 스럽게 억지로 바꾸고자 한다면 위의 4번 예를 볼 수 있다. 이 코드도 위의 경우처럼 직설적으로 번역될 처지이나 NS_SWIFT_NAME 이라는 독특한 매크로(preprocessor)를 이용해 Swift 용 별명을 만들어 줄 수 있다.
NS_SWIFT_NAME(init(anotherObjectName:));
위의 코드를 인터페이스 끝부분에 붙였는데, 이렇게 하면 anotherObjectWithName: 이라는 메소드는 Swift 에서 init(anotherObjectName:) 이라는 형식으로 인지할 수 있게 된다. 그래서 아래 처럼 사용할 수 있다.
let anotherBasicObject = MyGoodClass(anotherObjectName: "another basic user")
NS_SWIFT_NAME 은 Swift 용 별명을 지어주기 위한 매크로로 다방면에서 활용이 가능하다. 계속 살펴보자.

함수(Function)

C언어 문법으로 구현된 함수의 경우도 Swift 에서 그대로 쓸 수 있게 직역(?)된다. 아래 예제를 보자.
int SomeFunction(int a, int b);
위의 인터페이스의 경우 Swift 에서는 아래와 같은 식으로 액세스가 가능하다.
let result = SomeFunction(1, 2)
C 함수 입장에선 당연한 모양세지만 Swift 스럽지는 않은 모양이라고 생각된다.

이를 Swift 스럽게 잡아주기 위해서 NS_SWIFT_NAME 을 이용해 볼 수 있다. 함수 인터페이스를 아래처럼 고쳐보자.
int SomeFunction(int a, int b) NS_SWIFT_NAME(someFunction(a:b:));
NS_SWIFT_NAME 을 이용해 이름을 소문자로 시작하게 만들고 라벨까지 붙여 넣었다. 그래서 Swift 에서는 이제 아래 처럼 이 함수를 호출 할 수 있다.
let result = someFunction(a: 1, b:2)
당연히 이런 표현이 Swift 스럽다.

열거형(enum)

기본적인 C 열거형의 예를 보자.
typedef enum _my_good_type_ {
  MyGoodTypeDefault = 0,
  MyGoodTypeSolo,
  MyGoodTypeMulti
} MyGoodType;
이런 식으로 선언할 경우 MyGoodType 이라는 타입은 Swift 의 enum과는 다르게 struct 형태로 선언되어 버린다. 그래서 Swift 에서는 enum 스럽지 않게 액세스 해야한다.
let type: MyGoodType = MyGoodTypeDefault
점(.)으로 시작하지도 않고 하여간 Swift 의 enum 과는 맞지 않다.

이를 바로잡기 위해 Objective-C 스타일의 enum 을 정의, 즉 NS_ENUM 매크로를 이용해 열거형을 정의하게 되면 변화가 생긴다. 아래는 수정한 코드다.
typedef NS_ENUM(int, MyGoodType) {
  MyGoodTypeDefault = 0,
  MyGoodTypeSolo,
  MyGoodTypeMulti
};
이렇게 하면 MyGoodType 은 Swift 의 enum 형식으로 재정의 된다. 뿐만 아니라 각 이름 또한 Swift에 맞게 번역되어서 이름이 소문자로 시작하고 타입의 이름을 생략시킨다. 즉 아래처럼 쓸 수 있다.
let type: MyGoodType = .default
무척 Swift 스러워 졌다.

사실 이 정도만 봐도 왠만한 것은 해결되는 것인데, 이제 약간 억지 예제를 보자. 이름 떼기(?)가 불가능한 다른 이름을 하나 추가해 보자.
typedef NS_ENUM(int, MyGoodType) {
  MyGoodTypeDefault = 0,
  MyGoodTypeSolo,
  MyGoodTypeMulti,
  AnotherGoodTypeSuperSolo
};
굵게 처리한 한 항목을 추가했다. 보다싶이 이름이 전혀 어울리지 않는다. 완전 순 억지다. 물론 예를 위함이니 이해해 주셨으면 좋겠다.

하여간 MyGoodType 을 위처럼 수정해 버리면 위에서 예시한 Swift 코드가 오류가 발생시킬 것이다. 왜냐하면 이상한 이름이 하나 추가되는 바람에 이름의 전체에서 뺄 수 있던 요소(MyGoodType 이라는 이름)에 예외가 생기기 때문에 Xcode 에서 번역을 포기해 버리게 되기 때문이다. 실제로 테스트 해 보면 이름을 못 찾는다고 오류가 날 것이다.

이 문제를 바로잡기 위해서 추가된 한 녀석의 새로운 별칭을 지어보는 방법을 생각해 볼 수 있다. 물론 순 억지지만 말이다.
typedef NS_ENUM(int, MyGoodType) {
  MyGoodTypeDefault = 0,
  MyGoodTypeSolo,
  MyGoodTypeMulti,
  AnotherGoodTypeSuperSolo NS_SWIFT_NAME(anotherGoodSuperSolo)
};
이렇게 하면 기존에 쓰던 대로 .default 나 .solo 같은 이름은 그대로 쓸 수 있다. 거기다 새롭게 추가된 이름도 쓸 수 있다.
let firstType: MyGoodType = .default
let secondType: MyGoodType = .anotherGoodSuperSolo
당연하게도 Xcode 가 오해할 소지를 없애주면 아무 문제 없이 Swift 용 별명으로 번역해 준다.

마무리

사실 이 글은 NS_SWIFT_NAME 매크로에 대해 알게되어서 이에 대한 글을 쓰려고 하다보니 다른 글들과 똑같아 지는 것 같아서 관점을 다르게 보며 작성한 글이다. 이 NS_SWIFT_NAME 은 작년부터 개인적으로 애타게 찾아오던 정보였는데 정작 별로 쓸 일이 없어진 지금에서야 발견하게 되어 서글프기도 하다. :-(

이 글이 Swift 코더를 배려해야 하는 Objective-C 코더들에게 도움이 되었으면 좋겠다.

[참고글] Swift and Objective-C in the Same Project
[참고글] NS_SWIFT_NAME
[관련글] Swift 프로젝트에서 Objective-C 코드를 함께 사용하기
[관련글] 스위프트(Swift) 가이드

댓글 없음 :