콘텐츠로 건너뛰기
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;
}

“[Objective-C] 메소드를 동적으로 추가하기”의 13개의 댓글

  1. 염치 불구하고 질문 하나더 있습니다.
    데모 어디에서도 @dynamic propertyName; 와같은 문법을 사용하는 곳이 없는데요
    동적 메서드 바인딩과 @dynamic propertyName; 문법과 어떤 연관관계가 있는지 궁금합니다.
    어찌 보면 @dynamic propertyName; 문법과는 상관없이 class_addMethod 함수 활용한 동적 메서드 바인딩 같아서요

    1. 자답합니다.
      @interface
      @property void resolveThisMethodDynamically;
      @end
      @implementation
      @dynamic resolveThisMethodDynamically;
      @end
      상기 문구를 각 위치에 추가해주면 xcode상에서 ARC를 끄지 않아도 정상적으로 빌드가 가능합니다.
      감사합니다.

      1. @dynamic 지시어를 사용하는 예는 코어데이터외에는 저도 거의 못 본 것 같습니다. 이 지시어 자체의 의미가 “이 메소드는 동적으로 리졸빙하니 별도로 자동 작성하지 않는다” 정도로 이해하는데요. (@synthesize의 경우에는 컴파일러가 프로퍼티 접근자 메소드를 자동으로 작성합니다) 이 지시어를 사용하면 Xcode도 해당 접근자에 대해서는 실행시간에 결정되는 것으로 간주, 셀렉터를 찾을 수 없어도 무시하는 듯 하네요.
        감사합니다.

  2. sample로 제공된 소스 컴파일 되나요? 제가 소스 넣고 컴파일 해보니 resolveThisMethodDynamically 메서드가 선언되지 않았다고 하는데 추가적으로 resolveThisMethodDynamically을 선언해줘야 하나요? 선언해줘도 resolveInstanceMethod가 호출안되는데요?!

    1. 경고는 뜨는데요 (클래스메소드를 인스턴스에서 호출해서), 컴파일은 됩니다.
      첨부하신 그림을 보니, output 콘솔에 로그 메시지가 찍힌듯 한데요?
      음 그런데 앞뒤로 NSLog 찍으신 부분은 안나오네요;;; 읭?

        1. xcode에서는 semantic issue 오류 나는데, 저도 console에서 빌드 해봐야겠네요
          빠른 답변 감사합니다.

          1. console은 잘되네요;; 세미나 해야하는데 이를 어찌 설명해야 할까요;; ㅋ

      1. Log가 찍인 이유는 제가 resolveThisMethodDynamically를 인스턴스 메서드로 추가해보고 혹시 제가 추가한 resolveThisMethodDynamically 메서드가 불렸을 때 찍힌 Log입니다.
        지금 스샷에서는 빨간색 느낌표 Error로 인해 빌드 자체가 안되어서요 ㅜㅜ
        이글이 developer 문서를 단지 번역해둔 거라는건 알겠는데 어떻게 하면 동적으로 메서드를 추가해볼수 있을까요?

        1. 오류패널에서 에러는 ARC관련 한 내용이네요. 해당 파일에 대해 ARC를 끄고 컴파일 해보시면 왠지 될 거 같은 느낌이 듭니다.

          1. 느낌이 맞으세요 ARC 끄고 하니 xcode에서도 빌드 됩니다.
            성실한 답변 감사합니다.

      2. 참고로, 따로 선언을 안하고 쓰려는 게 이 예제의 목적입니다. `resolveInstanceMethod`는 NSObject의 클래스 메소드이구요. MyObject에 `resolveThisMethodDynamically`가 호출되면, 해당 셀렉터에 대응되는 인스턴스, 클래스 메소드가 없기 때문에 super의 디스패치 테이블을 뒤져볼텐데, 역시 해당 셀렉터는 없을 겁니다. 그럼 “알 수 없는 셀렉터”에 대한 예외를 일으키는 과정에서 NSObject에 미리 정의된 몇 몇 함수가 호출됩니다. `resolveInstanceMethod`는 그 중 한 과정이구요. 아마 제가 알기로는 최종적으로는 메시지를 포워딩하는 함수가 최종적으로 호출되는데, NSObject의 이 메소드의 디폴트 구현은 “알 수 없는 셀렉터” 예외를 일으키는 동작입니다.

    댓글이 닫혔습니다.