Objective-C 런타임

Objective-C는 동적 프로그래밍 언어로 Clang이나 GCC 같은 컴파일러에 의해 컴파일되어 실행되지만, 런타임 시스템은 동적으로 작동한다. 이것이 C언어 위에 동적인 객체 시스템을 구성하여 Foundation, Cocoa 같은 프레임웍을 만들게 된다.

이 런타임 시스템에 낮은 수준에서 접근하여 유용한 일을 할 수 있다. 이어질 다음 글에서 Cocoa에 사용되는 패턴 중 하나를 살펴보겠지만, 그 외에도 Foundation, Cocoa 등의 프레임웍이나 Objective-C와의 자동화된 스크립트 언어 바인딩을 구현하거나, 이 기반들과 호환되는 새로운 프로그래밍 언어를 만들 수도 있다. (Swift)

그중에서도 이 글에서는 런타임에 새로운 클래스를 선언하고, 그 클래스의 인스턴스를 생성하여 멤버 메소드를 호출해보는 것으로 Objective-C 런타임의 사용법을 간략히 소개한다. 프로토콜 등을 다루는 것은 새로운 프로그래밍 언어를 구현하지 않는 이상 이 글의 의도를 넘어선다. Objective-C 런타임 라이브러리의 구체적인 사용법에 대해서는 레퍼런스에서 볼 수 있다.1

이 글에서는 다음의 Objective-C 클래스 구현을 런타임에 하려 한다.

@interface Customer : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Customer
@end

실제로 컴파일된 결과는 위의 코드에서 자동으로 많은 부분이 생성synthesize된 것인데, 실제 모습은 (간략히) 다음과 같다.

@interface Customer : NSObject
@property (nonatomic, copy) NSString *name;
@end

@implementation Customer {
    NSString *_name;
}

- (NSString *)name {
    return _name;
}

- (void)setName:(NSString *)name {
    _name = [name copy];
}

@end

위 선언과 구현이 더 이상 없는 것으로 간주하고 런타임으로 코드를 옮겨본다. 편의상 Objective-C 코드도 사용되고 있지만, 이 글을 읽고 나면 스스로 완전한 C 코드로 바꿀 수 있다. 먼저 Customer라는 Class 객체를 선언하자. (편의상 전역 스코프를 가진다) 그리고 name 프로퍼티에 접근할 getter와 setter를 C 함수로 구현한다.

#include <objc/objc-runtime.h>

Class Customer;

NSString *name(id self, SEL _cmd) {
    Ivar ivar = class_getInstanceVariable(Customer, "_name");
    return object_getIvar(self, ivar);
}

void setName(id self, SEL _cmd, NSString *newName) {
    Ivar ivar = class_getInstanceVariable(Customer, "_name");
    id oldName = object_getIvar(self, ivar);
    if (oldName != newName) {
        object_setIvar(self, ivar, [newName copy]);
    }
}

실제로 Objective-C의 메소드 구현은 C 함수의 프로토타입(IMP라 한다)을 가진다. Objective-C의 메소드 호출은 메시지를 받을 객체에게 전달하는 것이지만, 메시지를 받은 객체는 메소드에 해당하는 실제 C 함수를 호출한다. 인스턴스 메소드 IMP 함수의 프로토타입은 Objective-C 메소드의 리턴형과 호출된 객체의 self, 호출된 메소드의 셀렉터 _cmd, 이후에 실제 메소드 인자들 순으로 나열된다. self_cmd는 Objective-C 메소드에서도 직접 참조가 가능한 값이다.2 아래에서 추가될 프로퍼티를 위한 인스턴스 변수3 _name을 어떻게 참조하는지 볼 수 있다. 이제 실제로 클래스를 정의한다.

Customer = objc_allocateClassPair([NSObject class], "Customer", 0);

NSObject 클래스를 부모 클래스로 하는 Customer라는 클래스를 생성한다.

class_addIvar(Customer, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));

Customer 클래스에 _name 인스턴스 변수를 추가한다. 마지막 인자에 런타임 시스템이 사용하는 실제 타입 정보를 제공한다. @encode는 Objective-C의 타입을 런타임 시스템이 사용하는 내부적인 타입으로 변환하는 컴파일러 지시자이다.4 런타임에 필요한 실제적인 타입 정보를 필요로 하는 곳은 모두 이 인코딩을 사용한다.

objc_property_attribute_t type = { "T", "@\"NSString\"" };
objc_property_attribute_t backingIvar = { "V", "_name" };
objc_property_attribute_t attrs[] = { type, backingIvar };
class_addProperty(Customer, "name", attrs, sizeof(attrs) / sizeof(attrs[0]));

위에서 추가한 _name 인스턴스 변수를 사용하는 name 프로퍼티를 추가한다.

class_addMethod(Customer, @selector(name), (IMP)name, "@@:");
class_addMethod(Customer, @selector(setName:), (IMP)setName, "v@:@");

name 프로퍼티의 getter와 setter 메소드를 추가한다. 객체 외부로 노출될 셀렉터와 내부 구현부인 IMP 함수가 지정된 것을 볼 수 있다. 마지막 인자는 위에서도 언급한 타입 정보로, @id 타입, vvoid 타입, :는 셀렉터 SEL 타입 등이다. 첫 번째는 리턴형이므로 두 번째와 세 번째는 모두 @:이다. self와 셀렉터 _cmd가 전달되기 때문이다.

객체가 모두 @인 것은5 런타임에서 모든 객체는 id 타입으로 메시지를 주고받기 때문이다. 이것이 Objective-C를 동적 언어로 만드는 중요한 부분이다.

objc_registerClassPair(Customer);

objc_registerClassPair 함수를 통해 클래스를 Objective-C 런타임에 등록한다. 이제 Customer 객체를 아래와 같이 생성할 수 있다.

id customer = class_createInstance(Customer, 0);
SEL getter = @selector(name);
SEL setter = @selector(setName:);
((void (*)(id, SEL, id))objc_msgSend)(customer, setter, @"John Appleseed");
NSString *name = ((id (*)(id, SEL))objc_msgSend)(customer, getter);

objc_msgSend 함수의 경우 AArch64 아키텍처가 도입되면서 프로토타입이 void 타입으로 변경되었다. 실제 호출될 IMP 함수의 프로토타입에 맞춰 캐스팅이 필요하다. Objective-C 문법으로 옮기면 아래와 같다.

id customer = [[Customer alloc] init];
[customer setName:@"John Appleseed"];
NSString *name = [customer name];

id 타입의 객체로 보내는 메시지에 대해서는 타입 검사를 하지 않기 때문에 앞서 메소드가 선언되지 않았더라도 컴파일러가 에러를 일으키지 않는다. 대신 프로퍼티 접근자를 사용하여 customer.name과 같이 접근할 수는 없다.

이런 방법이 실제 Cocoa 프레임웍에서 어떻게 활용되고 있는지 이어지는 글에서 살펴볼 예정이다.


  1. Objective-C Runtime (Apple Developer Documentation) ↩︎

  2. 따라서, 실제 Objective-C 메소드 구현과 동일한 코드를 IMP 함수에서 사용할 수 있다. ↩︎

  3. backing instance variable이라 부른다. ↩︎

  4. Type Encodings (Objective-C Runtime Programming Guide) ↩︎

  5. 실제로 id를 포함하여 NSObject 등의 객체 타입을 @encode하면 @ 값이 나온다. ↩︎