본문 바로가기
프로그래밍 놀이터/iOS

[Effective Objective-C] #12 메시지 포워딩을 이해하라

by 돼지왕왕돼지 2017. 8. 14.

 [Effective Objective-C] #12 메시지 포워딩을 이해하라


출처 : Effective Objective-C

@dynamic, @dynamic 프로퍼티, calayer, class_addMethod, Composite, coreanimation, CoreData, doesNotRecognizeSelect, doesnotrecognizeselector, Dynamic Method Resolution, effective objective-c, forwardingTargetForSelector, forwardingTargetForSelector, forwardInvocation, forwardInvocation, full forward mechanism, Hook, imp, key-value-coding-compliant, Message Forwarding, nil, nsinvocation, NSManagedObjects, nsobject, Pathway, replacement receiver, resolveClassMethod, resolveInstanceMethod, resolveInstanceMethod, return another receiver, runtime method add, SEL, synthesized, Terminating app due to uncaught exception, type encoding, unhandled selector exception, unknown selector, unrecognized, unrecognized selector sent to instance, v@:@, [Effective Objective-C] #12 메시지 포워딩을 이해하라, 다중 상속, 대체 리시버, 동적 메서드 해결, 디스패치 시스템, 런타임, 런타임 메서드, 런타임 메서드 추가, 리시버 교체, 메시지 변경, 메시지 포워드, 메시지 포워딩, 상위 클래스, 선택자, 셀렉터, 완전 포워드, 인스턴스 메서드, 인스턴스 변수, 인자, 접그자 메서드, 처리 비용, 캐싱, 컴파일 에러, 컴포지트, 크래시, 클래스 메서드, 타깃, 포워딩 경로, 포워딩 처리, 합성화


-

해석할 수 없는 메시지를 클래스에 보내는 것은 컴파일 시간 에러가 아니다.

컴파일러는 클래스에 없는 메시지를 보내는 코드를 컴파일할 때 에러를 일으키지 않는다.

메서드가 런타임에 추가될 수 있기 때문이다.

그래서 컴파일러가 메서드 구현이 존재하는지 여부를 알 수 있는 방법이 없다.



-

객체가 메시지를 받았을 때 그 메시지를 해석하지 못하면 메시지 포워드 단계로 넘어간다.

메시지 포워드는 해석할 수 없는 메시지를 처리하는 방법을 개발자가 객체에 알려주는 절차다.



-

콘솔에서 다음과 같은 메시지가 나오는 이유는 객체가 해석하지 못하는 메시지를 객체에 보냈기 때문이다.

-[__NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87

**** Terminating app due to uncaught exception ….


위의 오류는 NSObject 의 doesNotRecognizeSelect: 메서드에서 발생항 예외이다.

이 경우 포워딩 경로(pathway)는 애플리케이션 크래시로 끝난다.


그러나 여러분의 클래스에서 포워드 경로를 중간에 낚아채서(hook) 애플리케이션 크래시가 아닌 미리 정해진 로직을 수행할 수 있다.



-

포워딩 경로는 두 개 경로로 나뉜다.

첫 번째 경로는 해석할 수 없는 선택자 ( unknown selector ) 를 위한 메서드를 동적으로 추가할 기회를 리시버가 속한 클래스에 주는 것이다.

이를 동적 메서드 해결 (dynamic method resolution)이라고 한다.


두 번째 경로는 완전 포워드(full forward mechanism)이다.

런타임이 여기까지 왔다면 리시버가 선택자에 응답할 기회가 더는 없음을 알 수 있다.

그래서 스스로 요청을 처리할 것인지 리시버에 물어본다.

이 요청은 두 단계로 되어 있다.


첫 번째는 다른 객체가 대신 메시지를 받을 수 있는지 물어보는 것이다.

만약 그렇다면 런타임은 메시지를 우회시키고 모든 것은 정상으로 처리될 것이다.


대체 리시버(replacement receiver)가 없으면 완전 포워드가 작동된다.

NSInvocation 객체를 이용하여 아직 처리되지 않은 메시지의 모든 상세를 감싸버린다.

그리고 이 객체를 처리할 마지막 기회를 리시버에게 준다.




동적 메서드 해결


-

해석하지 못하는 메시지를 객체에 전달했을 때 가장 먼저 호출되는 메서드는 객체의 클래스에 있는 클래스 메서드다.

+ (BOOL)resolveInstanceMethod:(SEL)selector


선택자를 처리할 수 있는 인스턴스 메서드가 클래스에 추가되어 있는지 여부를 가르키는 불린을 반환한다.

존재한다면, 클래스는 나머지 포워드 방법으로 넘어가기 전에 구현을 추가할 수 있는 두 번째 기회를 얻을 수 있다.

비슷한 메서드인 resolveClassMethod: 는 미구현된 메서드가 인스턴스 메서드가 아니라 클래스 메서드일 때 호출된다.


이 방법은 사용할 준비가 된 메서드의 구현이 있을 때만 가능하다.

이는 메서드가 클래스에 동적으로 합쳐질 준비가 된 상태를 말한다.

이 메서드는 CoreData 에 있는 NSManagedObjects 의 프로퍼티에 접근할 때 생기는 @dynamic 프로퍼티를 구현할 때 가끔 사용된다.

그런 프로퍼티를 구현할 필요가 있는 메서드는 컴파일 시간에 알 수 있기 때문이다.




리시버 교체


-

해석할 수 없는 선택자를 처리하기 위한 두 번째 시도는 대체 리시버가 대신 메시지를 처리할 수 있는지 리시버한테 물어보는 것이다.

이것을 처리하는 메서드는 다음과 같다.


- (id)forwardingTargetForSelector:(SEL)selector


이 메서드에 해석할 수 없는 선택자를 인자로 전달하면 대체 리시버를 반환하거나 대체 리시버를 찾을 수 없으면 nil 을 반환할 것이다.

이 메서드는 컴포지트(composite) 패턴을 함께 이용해서 다중 상속의 이점을 일부 제공할 수 있다.



-

이 포워드 경로에서는 메시지를 수정할 수 있는 부분이 없다는 것을 알고 있어야 한다.

메시지를 대체 리시버로 보내기 전에 수정해야 한다면 완전 포워드를 사용해야만 한다.




완전 포워드


-

완전 포워드는 NSInvocation 객체를 생성하는 것부터 시작한다.

이 객체는 처리되지 않은 메시지의 모든 내용을 감싼다.

이 객체는 선택자, 타깃, 인자를 포함한다.

NSInvocation 객체가 호출되면 메시지 디스패치 시스템이 동작하고 메시지를 객체의 타깃으로 전달한다.


- (void)forwardInvocation:(NSInvocation*)invocation


이 메서드는 호출의 타깃을 변경하고, 변경된 것을 호출하는 방법으로 간단하게 구현할 수 있다.

그러나 이 방법은 대체 리시버 메서드를 사용하는 것과 동일하다.

그래서 이런 방법은 거의 사용되지 않는다.

호출하기 전에 특정 방법으로 메시지를 변경하는 편이 좀 더 유용한 구현 방식이다.

인자를 추가하거나 선택자를 변경하는 식으로 구현할 수 있다.



-

이 메서드를 구현할 때는 처리하지 못하는 호출을 해결하기 위해 상위 클래스의 구현을 항상 호출해야 한다.

이 말은 계층 구조의 상위 클래스 모두가 이 호출을 처리할 수 있는 한 번의 기회를 얻는다는 것을 의미한다.

결국엔 NSObject 의 구현이 호출될 것이고, 미처리 선택자 에외(unhandled selector exception)을 일으키는 doesNotRecognizeSelctor: 가 호출될 것이다.






전체적인 그림


-

각 단계는 이전 방법보다는 처리 비용이 비싸다.

가장 좋은 경우는 메서드가 첫 단계에서 해결되는 것이다.

해결된 메서드는 런타임에 캐싱이 되고, 이후에 같은 클래스의 인스턴스에 대한 같은 선택자의 호출은 포워딩 처리를 하지 않아도 된다.



-

두번째 단계에서 메시지를 다른 리시버에 전달하는 것은 대체 리시버를 찾을 수 있는 상황에서의 세 번째 단계를 간단히 최적화 한 것이다.

그 경우 호출에 대해 변경해야 하는 것은 타깃뿐이다.



-

마지막 단계는 NSInvocation 을 생성하고 처리해야 해서 이보다 훨씬 복잡하다.



-

1.

resolveInstanceMethod 

     -> YES ( return YES ) : 메시지를 처리함

     -> NO ( return NO ) : forwardingTargetForSelector



-

2.

forwardingTargetForSelector

     -> YES ( return another receiver ) : 메시지를 처리함

     -> NO ( return nil ) : forwardInvocation



-

3.

forwardInvocation

     -> YES : 메시지를 처리함

     -> NO : 메시지를 처리하지 못함




동적 메서드 해결 전체 예제

-

@dynamic 프로퍼티를 이용해 동적 메서드 해결을 사용하는 것을 보여준다.

구현부에서 프로퍼티를 @dynamic 으로 선언하면 인스턴스 변수와 접근자 메서드가 자동으로 생성되지 않는다.


EOCAutoDictionary 의 인스턴스 프로퍼티에 대한 호출을 처음 할 때는 정확한 선택자를 찾을 수 없다.

직접 구현되어 있지 않고, 합성화(synthesized)도 되지 않았기 때문이다.


resolveInstanceMethod 에서 함수를 추가한다.

런타임 메서드인 class_addMethod 를 이용한다.

이 메서드는 주어진 선택자와 함수 포인터로 주어진 구현과 함께 동적으로 클래스에 메서드를 추가한다.


이 함수의 마지막 파라미터는 구현에 대한 타입 인코딩(type encoding)이다.

타입 인코딩은 반환형을 나타내는 문자열과 함수로 전달된 파라미터들로 이루어져 있다.


// header

@interface EOCAutoDictionary : NSObject

@property (nonatomic, strong) NSString *string;

@property (nonatomic, strong) NSNumber *number;

@property (nonatomic, strong) NSDate *date;

@end


// implementation

@interface EOCAutoDictionary ()

@property (nonatomic, strong) NSMutableDictionary *backingStore;

@end


@implementation EOCAutoDictionary

@dynamic string, number, date;


- (id)init{

     if ((self = [super init])){

          _backingStroe = [NSMutableDictionary new];

     }

     return self;

}


+ (BOOL)resolveInstanceMethod:(SEL)selector{

     NSString *selectorString = NSStringFromSelector(selector);

     if ([selectorString hasPrefix:@“set”]){

          class_addMethod( self, selector, (IMP)autoDictionarySetter, “v@:@“);

     } else{

          class_addMethod( self, selector, (IMP)autoDictionaryGetter, “@@:”);

     }

}



id autoDictionaryGetter(id self, SEL _cmd){

     EOCAutoDictionary *typedSelf = (EOCAutoDictionary*) self;

     NSMutableDictionary *backingStore = typedSelf.backingStore;


     NSString *key = NSStringFromSelector(_cmd);

     return [backingStore objectForKey:key];

}



void autoDictionarySetter(id self, SEL _cmd, id value){

     EOCAutoDictionary *typedSelf = (EOCAutoDictionary*) self;

     NSMutableDictionary *backingStore = typedSelf.backingStore;


     // set prefix 와 : suffix 제거하는 과정


     if ( value ){

          [backingStore setObject:value forKey:key];

     } else{

          [backingStore removeObjectForKey:key];

     }

}


@end



-

이와 비슷하게 구현된 것이 CoreAnimation 프레임워크의 CALayer 이다.

CALayer 는 이 방법으로 키-값 코딩 호환(key-value-coding-compliant) 컨테이너 클래스가 될 수 있다.

이 클래스의 모든 키에 대해 값을 저장할 수 있는 클래스다.




기억할 점


메시지 포워딩은 객체가 선택자에 응답할 수 없다는 사실을 알았을 때 진행되는 절차다.


동적 메서드 해결은 런타임에 메서드를 클래스에 추가하고, 바로 사용할 때 이용된다.


객체는 해석할 수 없는 선택자를 다루기 위해 다른 객체를 선언할 수 있다.


완전 포워딩은 이전 두 방법으로 선택자를 처리할 수 없을 때 호출된다.




댓글0