[Objective-C] 클래스,인스턴스 메소드를 동적으로 추가하기

동적 메소드 변형

메소드를 동적으로 추가하기

(도대체 언제인지는 감을 잡기 힘들지만) 객체에 동적으로 기능의 구현을 추가해야 할 경우가 이따금씩 있다. 예를 들자면 @dynamic 지시어를 써서 프로퍼티를 선언하는 경우가 이에 해당한다.

@dynamic propertyName;

이 구문은 컴파일러에게 프로퍼티와 연관되는 메소드가 동적으로 제공된다는 것을 알려주게 된다.

이러한 동적 메소드 할당을 위해서는 resolveInstanceMethod:resolveClassMethod:를 구현해서 특정 셀렉터를 클래스 메소드 혹은 인스턴스 메소드에 동적으로 추가할 수 있다.

메시징 구현에서 살펴보았듯이 Objective-C의 메소드는 단순히 2개의 핵심 인자(self_cmd)를 받는 C함수이다. 이러한 형태로 함수를 정의해두면, class_addMethod 함수를 사용하여 이 함수를 인스턴스나 클래스 메소드로 추가할 수 있다.(이 둘의 구분은 class_addMethod 함수가 위의 두 메소드 중 어디에서 호출되었느냐에 따라 달라진다.) 예를 들어 다음과 같은 함수를 정의했다고 하자.

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

이 함수는 다음과 같이 어떤 클래스의 인스턴스 메소드로 동적으로 연결될 수 있다. 이 때 새로 만들어지는 메소드의 이름은 resolveThisMethodDynamically 이다.

@implementation MyClass
+(BOOL) resolveInstanceMethod:(SEL)aSEL {
    if (aSEL == @selector(resolveThisMethodsDynamically)) {
        class_addMethod([self class], aSEL, (IMP) dynamicallyMethodIMP, "v@:");
        return YES;
    }

    return [super resolveInstanceMethod:aSEL];
}

class_addMethod 함수의 원형은 다음과 같다.

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

여기서 IMP은 실제 셀렉터를 통해 호출해야 할 함수의 포인터를 가리킨다. 그 정의는 다음과 같고 의미는 "함수의 구현"이다..

id (*IMP)(id, SEL, ...);

함수의 타입은 types에 인코딩된 타입문자열로 들어간다. 여기서 실제 구현함수인 dynamicallyMethodIMP는 id 형과 SEL 형을 인자로 받으며, 리턴값은 없다.(void) 따라서 이 함수의 타입을 인코딩하면 "v@:" 이고 이는 void-id-SEL 의 의미가 된다.

메소드를 포워딩하는 것과 동적 메소드 변경은 대략 대조를 이룬다. 클래스는 포워딩 매커니즘이 끼어들기 이전에 메소드를 동적으로 변경할 수 있는 기회를 가진다. 만약 respondsToSelector:instancesRespondsToSelector:가 호출되면, 동적 메소드 변경자가 해당 셀렉터에 대해 IMP를 제공할 기회를 가지게 된다. resolveInstanceMethod:를 구현했는데, 특정한 셀렉터에 대해서는 포워딩하고 싶다면 해당 셀렉터에 대해서 NO를 리턴하도록 하면 된다.

동적 로딩

Objective-C 프로그램은 실행중에 새로운 클래스와 새로운 카테고리를 링크할 수 있다. 새 코드는 프로그램과 상호작용할 수 있고, 맨 처음부터 있었던 것들과 동등하게 취급된다. 동적인 로딩으로 많은 것들을 할 수 있다. 예를 들어 시스템 환경 설정 앱의 많은 모듈들은 모두 동적으로 로딩된다.

코코아 환경에서 동적 로딩은 애플리케이션의 커스터마이징을 위해 흔히 사용된다. 다른 누군가가 당신이 작성한 앱이 실행되는 중간에 로딩될 수 있는 모듈을 작성할 수 있다. 이는 인터페이스 빌더가 커스텀 팔레트를 로딩하는 것이나, OSX의 환경 설정앱이 커스텀 모듈을 읽어들이는 것과 비슷한다. 로드가능한 모듈은 애플리케이션이 할 수 있는 일을 확장시킨다. 대신에 이들은 당신이 허용한 방법으로만 프로그램에 기여할 수 있다. 이를 테면 당신은 프레임워크를 제공하고 다른 사람들이 코드를 제공하는 것이다.

Mach-O 파일들에 있는 Objective-C 모듈을 동적으로 로딩하는 런타임 함수가 있지만, 코코아의 NSBundle 클래스는 이러한 동적 로딩에 대해 훨씬 더 편리한 사용성을 제공한다.

예제

  • 진상

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

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

      • 저도 로그찍는 부분 추가해서 컴파일해보니 아래와 같이 실행됩니다.

        • 진상

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

          • 진상

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

      • 진상

        Log가 찍인 이유는 제가 resolveThisMethodDynamically를 인스턴스 메서드로 추가해보고 혹시 제가 추가한 resolveThisMethodDynamically 메서드가 불렸을 때 찍힌 Log입니다.

        지금 스샷에서는 빨간색 느낌표 Error로 인해 빌드 자체가 안되어서요 ㅜㅜ

        이글이 developer 문서를 단지 번역해둔 거라는건 알겠는데 어떻게 하면 동적으로 메서드를 추가해볼수 있을까요?

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

          • 진상

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

          • 해결되셨다니 다행입니다. 좋은 하루 보내세요~

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

  • 진상

    염치 불구하고 질문 하나더 있습니다.

    데모 어디에서도 @dynamic propertyName; 와같은 문법을 사용하는 곳이 없는데요
    동적 메서드 바인딩과 @dynamic propertyName; 문법과 어떤 연관관계가 있는지 궁금합니다.

    어찌 보면 @dynamic propertyName; 문법과는 상관없이 class_addMethod 함수 활용한 동적 메서드 바인딩 같아서요

    • 진상

      자답합니다.

      @interface
      @property void resolveThisMethodDynamically;
      @end

      @implementation
      @dynamic resolveThisMethodDynamically;
      @end

      상기 문구를 각 위치에 추가해주면 xcode상에서 ARC를 끄지 않아도 정상적으로 빌드가 가능합니다.
      감사합니다.

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

        감사합니다.