Runtime 시스템 개요
Objective-C 는 정적 컴파일 언어가 아닌 동적 언어로 분류됩니다. 이는 많은 결정 사항들이 컴파일 시점이 아닌 실행 시점 (Runtime) 에 이루어진다는 것을 의미합니다. 컴파일러만으로는 충분하지 않으며, 컴파일된 코드를 실제로 실행하기 위해 런타임 시스템이 필요합니다. Objective-C Runtime 은 바로 이 역할을 수행하며, Objective-C 언어 체계의 근간을 이룹니다.
C 언어의 경우 함수 호출이 컴파일 시점에 바인딩되지만, Objective-C 는 메시징 기반의 동적 호출 방식을 사용합니다. 컴파일 단계에서는 어떤 함수가 호출될지 확정되지 않으며, 실제 싖 환경에서 메서드 이름을 기반으로 적절한 구현체를 찾아 호출합니다. 이러한 높은 동적 성격을 구현하기 위해 Runtime 은 주로 C 와 어셈블리로 작성되어 성능 최적화가 이루어져 있습니다.
Runtime 헤더 파일 및 주요 타입
iOS SDK 의 usr/include/objc 디렉토리에는 런타임 관련 헤더 파일들이 포함되어 있습니다. 그중에서도 message.h 와 runtime.h 가 가장 핵심적입니다. 전자는 객체에게 메시지를 전송하는 저수준 함수들을 정의하며, 후자는 런타임 시스템을 조작하는 다양한 API 를 제공합니다.
사용 시에는 다음과 같이 헤더를 임포트해야 합니다.
#import <objc/message.h>
#import <objc/runtime.h>
runtime.h 에는 런타임 조작을 위한 핵심 타입들이 정의되어 있습니다. 클래스, 메서드, 인스턴스 변수 등을 불투명 포인터 타입으로 추상화하여 제공합니다.
// 클래스 정의 내의 메서드를 나타내는 타입
typedef struct objc_method *Method;
// 인스턴스 변수를 나타내는 타입
typedef struct objc_ivar *Ivar;
// 카테고리를 나타내는 타입
typedef struct objc_category *Category;
// Objective-C 프로퍼티를 나타내는 타입
typedef struct objc_property *objc_property_t;
// 클래스 자체를 나타내는 타입 (objc.h 정의)
typedef struct objc_class *Class;
이러한 타입 정의를 통해 클래스의 내부 구조를 세부적으로 분해하고 조작할 수 있습니다. Objective-C 2.0 이후 일부 구조체 멤버는 직접 접근이 제한되었지만 (OBJC2_UNAVAILABLE), 대신 제공되는 접근자 함수를 통해 안전하게 값을 얻을 수 있습니다.
Class userClass = [User class];
// 직접 구조체 접근은 불가능하며, 전용 함수 사용
const char *className = class_getName(userClass);
NSLog(@"%s", className); // 출력: User
API 네이밍 컨벤션
Runtime 함수들은 접두사를 통해 조작 대상에 따른 계층 구조를 명확히 보여줍니다.
object_: 객체 인스턴스 조작class_: 클래스 객체 조작method_: 메서드 조작ivar_: 인스턴스 변수 조작property_: 프로퍼티 조작protocol_: 프로토콜 조작
또한 objc_ 접두사를 가진 함수들은 런타임 시스템 전체를 관리하며, 메모리 상의 클래스 목록 조회나 연관 객체 관리 등 광범위한 작업을 수행합니다. 예를 들어, 현재 실행 중인 앱에 로드된 모든 클래스 목록을 출력하는 코드는 다음과 같습니다.
- (void)analyzeLoadedClasses {
unsigned int totalClasses = 0;
Class *classList = objc_copyClassList(&totalClasses);
for (unsigned int i = 0; i < totalClasses; i++) {
const char *name = class_getName(classList[i]);
printf("%s\n", name);
}
}
메시지 전송 메커니즘
Objective-C 에서 메서드 호출은 본질적으로 객체에게 메시지를 보내는 행위입니다. 컴파일러는 [object method] 형태의 코드를 objc_msgSend 함수 호출로 변환합니다.
// 일반적인 메서드 호출
User *user = [[User alloc] init];
[user authenticate];
//底层 메시지 전송 형태로 변환됨
objc_msgSend(user, @selector(authenticate));
// 클래스 메서드 호출 역시 객체 (메타클래스) 에게 메시지 전송
[User login];
// 변환됨: objc_msgSend([User class], @selector(login));
컴파일 옵션 -rewrite-objc 를 사용하면 clang 을 통해 소스 코드가 어떻게 변환되는지 직접 확인해 볼 수 있습니다.
메서드 구현 교환 (Method Swizzling)
기존 코드를 수정하지 않고 특정 메서드의 동작을 변경해야 할 때 메서드 구현 교환 기법을 사용합니다. 주로 로그 기록, 성능 분석, 기능 확장 등의 목적으로 활용됩니다. 클래스가 메모리에 로드될 때 한 번만 호출되는 load 메서드 내에서 구현하는 것이 안전합니다.
예를 들어, 모든 뷰 컨트롤러의 viewDidLoad 호출 시점을 로그로 남기려면 다음과 같이 구현할 수 있습니다.
@implementation UIViewController (Logging)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// 원래 메서드와 교체할 메서드 선택자 획득
SEL originalSelector = @selector(viewDidLoad);
SEL swizzledSelector = @selector(logged_viewDidLoad);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 메서드 구현 교환
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)logged_viewDidLoad {
// 교환되었으므로 실제로는 원래 viewDidLoad 가 호출됨
[self logged_viewDidLoad];
NSLog(@"View Loaded: %@", NSStringFromClass([self class]));
}
@end
연관 객체 (Associated Objects)
카테고리를 사용하여 클래스에 새로운 프로퍼티를 추가할 때, 인스턴스 변수를 직접 추가할 수 없으므로 연관 객체 기능을 사용합니다. 이는 객체에 키 - 값 쌍을 동적으로 바인딩하는 방식입니다.
예를 들어, 시스템 클래스인 UIView 에 고유 식별자 프로퍼티를 동적으로 추가하는 사례입니다.
static char kUniqueIdentifierKey;
@implementation UIView (Identifier)
- (NSString *)uniqueID {
return objc_getAssociatedObject(self, &kUniqueIdentifierKey);
}
- (void)setUniqueID:(NSString *)uniqueID {
objc_setAssociatedObject(self,
&kUniqueIdentifierKey,
uniqueID,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
또한, 콜백 객체 (예: 버튼 타겟) 에 추가 데이터를 전달할 때도 유용합니다. UIAlertView 대신 UIButton 에 데이터를 연관시켜 관리할 수 있습니다.
- (void)configureButton:(UIButton *)button withData:(NSDictionary *)data {
// 버튼에 데이터 연관시키기
objc_setAssociatedObject(button, "action_data", data, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[button addTarget:self action:@selector(buttonTapped:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)buttonTapped:(UIButton *)sender {
NSDictionary *data = objc_getAssociatedObject(sender, "action_data");
// 연관된 데이터 활용 로직
}
objc_setAssociatedObject 의 네 번째 파라미터인 정책 (policy) 은 메모리 관리 방식 (Retain, Copy, Assign 등) 을 결정합니다.
동적 메서드 해결 (Dynamic Method Resolution)
존재하지 않는 메서드를 호출하려 할 때, 런타임은 클래스에 resolveInstanceMethod: 와 같은 기회를 제공하여 동적으로 메서드를 추가할 수 있게 합니다. 이는 메서드 구현 테이블을 지연 생성하여 리소스를 절약하거나, 특정 프로토콜을 동적으로 준수하게 만들 때 사용됩니다.
@implementation TaskManager
- (void)viewDidLoad {
[super viewDidLoad];
TaskManager *manager = [[TaskManager alloc] init];
// 구현되지 않은 메서드 호출 시도
[manager performSelector:@selector(executeTask)];
}
// 구현되지 않은 인스턴스 메서드 호출 시 인터셉트
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(executeTask)) {
// 동적으로 메서드 추가
// 타입 인코딩: void 반환 (@: self, :_cmd)
class_addMethod(self, sel, (IMP)dynamicTaskImplementation, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void dynamicTaskImplementation(id self, SEL _cmd) {
NSLog(@"Dynamic task executed on %@", self);
}
@end
Runtime 를 활용한 딕셔너리 - 모델 변환
KVC(Key-Value Coding) 를 사용하면 딕셔너리 데이터를 모델 객체에 쉽게 매핑할 수 있지만, 존재하지 않는 키에 대해 오류가 발생할 수 있습니다. Runtime 를 이용하여 모델의 인스턴스 변수를 순회하며 딕셔너리 데이터를 안전하게 바인딩하는 방법을 구현할 수 있습니다.
+ (instancetype)objectWithJSON:(NSDictionary *)json {
id instance = [[self alloc] init];
unsigned int count = 0;
// 클래스의 인스턴스 변수 목록 획득
Ivar *ivarList = class_copyIvarList(self, &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivarList[i];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
// 인스턴스 변수는 보통 '_' 로 시작하므로 제거
if ([key hasPrefix:@"_"]) {
key = [key substringFromIndex:1];
}
id value = json[key];
if (value) {
// 중첩된 딕셔너리 처리 등 추가 로직 가능
[instance setValue:value forKey:key];
}
}
free(ivarList);
return instance;
}
Runtime 메시지 Lookup 흐름
메서드 호출 시 런타임은 다음과 같은 순서로 구현체를 찾습니다.
- 객체의
isa포인터를 통해 클래스 구조체를 찾습니다. - 클래스의 메서드 캐시 (Cache) 에서 선택자 (
SEL) 에 해당하는 구현체 주소를 검색합니다. - 캐시에 없다면 메서드 목록 (Method List) 을 순회하며 찾습니다.
- 현재 클래스에서 찾지 못하면 슈퍼클래스로 연쇄적으로 검색을 계속합니다.
- 최종적으로 구현체를 찾지 못하면 메시지 전달 체인 (
forwardingTarget,forwardInvocation등) 을 통해 오류 처리 또는 대체 실행을 시도합니다.
이러한 동적 바인딩 과정 덕분에 Objective-C 는 강력한 유연성을 가지지만, 동시에 런타임 오버헤드가 발생할 수 있음을 이해해야 합니다.