콘텐츠로 건너뛰기
Home » [Objective-C] 메소드를 동적으로 추가하기

[Objective-C] 메소드를 동적으로 추가하기

보통 객체에 필요한 메소드는 해당 객체의 클래스에서 미리 정의된다. 그러나 경우에 따라서는 객체에 동적으로 메소드를 추가해야 하는 경우가 있다. (도대체 언제?) 실행 시간에 동적으로 메소드를 추가하는 경우는 상상하기 어려운데, 코어 데이터를 사용할 때 자동으로 생성되는 모델 클래스에서 볼 수 있는 @dynamic 지시어가 붙은 프로퍼티 선언이 여기에 해당한다고 볼 수 있다.

코어데이터의 모델 클래스는 Xcode에 의해서 자동으로 생성되는데, 이 때 프로퍼티는 선언만 되고 이를 정의하는 코드는 자동으로 생성되지 않는다. 대신 @dynamic 이라는 지시어는 해당 프로퍼티의 접근자들이 나중에 동적으로 제공될 것이라는 것을 알려주게 된다. 이러한 동적 메소드를 붙여주는 기능을 구현하기 위해서는 해당 클래스의 원형에 -resolveInstanceMethod:-resolveClassMethod: 를 구현해서 원하는 셀렉터를 동적으로 추가할 수 있다.


Objective-C의 메시지(메소드)는 결국 C언어인데, OBJ-C 코드에서 보이는 것과 달리 C함수에는 “self”와 “_cmd” 라는 핵심 파라미터가 추가로 정의되어 있는 형태이다. 따라서 이러한 모양에 맞춰서 C함수를 작성해두면, 나중에 이러한 C 함수를 특정 객체나 클래스의 메소드로 추가할 수 있다.

예를 들어 아래와 같은 C함수가 있다고 하자.

void dynamicallyMethodIMP(id self, SEL _cmd) 
{ 
   /* .... */
}

MyClass 라는 클래스를 하나 만들었다고 가정하자. 이 클래스의 인스턴스 객체에 메소드를 호출하면 대충 다음과 같은 과정을 거치게 된다.

  1. 해당 객체의 isa 포인터를 따라 클래스 객체를 찾고, 해당 클래스의 셀렉터 테이블에서 메소드를 찾는다.
  2. 1에서 발견된다면 해당 함수를 호출한다.
  3. resolveInstanceMethod:를 호출한다.
  4. 위 메소드가 NO를 리턴하면 메시지를 다른 객체에 포워딩할 수 있는지 확인한다.
  5. YES라면 2로 돌아간다.

즉 Objective-C에서는 찾을 수 없는 이름의 메시지를 객체에 보내면, 바로 예외가 발생하는 것이 아니라 이를 처리할 어떤 지점을 하나 만들어놓은 것이라고 이해하면 된다. 그리고 이 시점에 아직은 존재하지 않았던 메소드를 처리하는 동작이 들어간다. 이 글에서 소개하는 것처럼 새로운 메소드를 추가해주어도 되고, 다른 객체의 메소드를 호출해도 된다.

아래 코드는 특정한 메시지의 셀렉터를 동적으로 클래스에 메소드로 추가하는 코드이다. 여기에는 class_addMethod() 라는 마법의 함수가 등장한다. 이 함수는 클래스와, 셀렉터, 구현체함수포인터와 타입 시그니처 정보등을 인자로 받는다.

@implementation MyClass

+(BOOL) resolveInstaceMethod: (SEL)aSelector 
{
    if (aSelector == @selector(resolveThisMethodDynamically)) {
        class_addMethod([self class], aSelector, (IMP)dynamicallyMethodIMP, "v@:");
        return YES;
    }
    return [super resolveIntanceMethod:aSelector];
}

// prototype of `class_addMethod()` 
// BOOL class_addMethod(Class cls, SEL name, IMP imp, const char* types);

함수의 타입은 각각의 타입을 특정한 문자로 표현하기로 약속하고, 이 규칙에 따라 인코딩된 문자열이 된다. "v@:"void id SEL 에 각각 해당한다.

이렇게 동적으로 메소드를 변경하는 것은, 당장 처리 불가능한 메시지를 처리한다는 맥락은 비슷하지만 구분된다. 메시지 처리 프로세스에서는 포워딩 매커니즘이 끼어들기 전에, 이 메소드 동적 변경 기회가 생긴다. 또한 직접적인 호출 시점 이전에, 어떤 클래스에 대해서 respondsToSelector:instancesRespondsToSelector: 메시지가 전달되었을 때에도 클래스는 해당 셀렉터에 대해서 동적인 구현을 제공할 기회를 가지게 된다.

동적 로딩

Objective-C 프로그램은 실행 중인 상태에서도 새로운 클래스와 새로운 카테고리 같은 것들을 로딩하고 링크할 수 있다. 새롭게 로딩된 코드는 프로그램이 시작될 때 로딩된 부분과 마찬가지로 프로그램과 상호작용하면서 작동할 수 있다. 이 기법은 실제로도 많이 사용되는데, macOS의 시스템 환경 설정앱은 대부분이 동적으로 로딩되는 요소로 구성된다.

코코아 환경에서 동적 로딩은 애플리케이션의 커스터마이징을 위해서도 흔히 사용된다. 인터페이스 필더가 제3자 플러그인이 제공하는 커스텀 팔레트를 로딩하는 것, OSX의 환경설정 앱이 커스텀 모듈을 읽어들이는 것도 이와 비슷하다. 추가로 로드할 수 있는 모듈이 있다면 애플리케이션을 확장할 수 있다. 단, 이러한 모듈은 앱이 허용한 방법으로만 프로그램에 영향을 끼칠 수 있다.

#import <Foundation/Foundation.h>

// 동적으로 추가할 메소드
void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"This Method(%@) is dynamically resolved.", NSStringFromSelector(_cmd));
}


@interface MyObject:NSObject
// 별도의 메소드를 정의한 적 없음
{
  int i;
}
@end


@implementation MyObject
// 'resolveThisMethodDynamically' 라는 메시지를 받으면 위에서 정의한 함수로
// 메소드를 추가하기 
+(BOOL)resolveInstaceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically)) {
        class_addMethod([self class], aSEL, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    
    return [super resolveInstanceMethod:aSEL];
}
@end

// 테스트
int main(int argc, const char** argv) 
{
    @autoreleasepool {
    MyObject *m = [[MyObject alloc] init];
    [m resolveThisMethodDynamically];  // <- 동적으로 추가될 메소드
    [m release];
    }
    return 0;
}