본문 바로가기
아이폰 개발/Objective C

objective c 클래스 클러스터의 서브 클래스

by 인생여희 2020. 12. 30.

objective c 클래스 클러스터의 서브 클래스

 

 

NSObject는 클래스 클러스터로 구성되지 않기 때문에 이것의 서브클래스를 만드는것은 전혀 문제가 없다. 그러나 기초 프레임워크에서 제공하는 NSObject  이외의 클래스의 서브클래스를 만드는 것에는 신중하여야 한다. 애플 문서에도 명시되어 있지 않은 클래스 클러스터가 숨어 있기 때문이다. 

 

예를들면 NSString의 서브 클래스를 사용자가 만들경우 실제 동작하던 NSString의 메소드는 비공개 서브 클래스의 메소드이므로 서브클래싱이 형성되면서 기존의 메소드는 동작하지 않게 된다. 이 문제를 해결하려면 아래와 같은 몇가지 방버이 있다.

 

1. 서브클래스 대신 카테고리로 사용자가 원하는 메소드를 추가한다. 대신 인스턴스 변수 추가가 불가능하기 때문에 완전한 서브클래스 구현에는 한계가 있다. 

 

 

 

 

2.상속할 클래스 (S라고 가정)의 인스턴스를 새로 만드는 클래스 (C )의 인스턴스 변수로 만들고 S 클래스가 처리할 메시지(메소드 호출)를 인스턴스 변수로 우회 시킨다. 

 

 

이 방법은 서브클래스를 구현하는데 구성연관 으로 대신 하는 방법이다. 메소드 처리라는 관점에서 서브클래스는 상위클래스의 메소드 처리 기능을 그대로 사용하기 위한 것이다. 이를 해결하기 위해 C클래스(새로만들 서브클래스)가 자체적으로 처리가 되지 않는 메시지를 자신이 가지고 있는 가상 슈퍼 클래스의 인스턴스에 전송하여 처리 의뢰하는 것이다. obj c는 이것을 메지시 포워딩이라고 한다. 

 

objc 에서 현재 클래스에서 처리가 되지 않는 메시지를 만났을 때는 아래와 같이 메시지 포워딩 메소드 forwardInvocation 을 이용하여 anInvocation 메시지 정보를 상위 클래스로 전송한다. 최종적으로 NSObjects는 더 이상 상위 클래스가 없는 관계로 doesNotRecognizeSelector 메소드를 호출하여 에러 메시지를 생성하는 예외처리를 한다. 

 

 

 

 

새로 만들 클래스 C 내부에 상위 클래스처럼 구현할 클래스 S의 인스턴스를 content라고 가정하고 위에서 알아본 forwardInvocation 메소드는 클래스 C에서 다음과 같이 오버라이드 한다.

 

 

 

 

위의 예에서 메시지 정보를 가지고 있는 NSInvocation 클래스의 anInvocation 객체는 selector와 target이라는 속성(프로퍼티)을 통해 메시지 정보를 얻어 올 수 있다. 또한 invokeWithTarget 이라는 메소드를 통해 다른 객체로 우회하여 메시지를 다시 전송할 수 있다. 

 

 

아래처럼 완전한 구현을 위해 클래스 C의 두가지 메소드도 오버라이드 한다. 즉, 메시지 포워딩을 위해서는 모두 세 개의 메소드를 오버라이드하여야 한다.

 

 

 

메시지 포워딩을 위해 오버라이드하는 세 개의 메소드는 인스턴스 변수 이름 (여기서는 content)만 바꾸면 어디에나 적용 가능하기 때문에 위의 모듈을 그대로 복사하여 활용하면 된다. 코드의 내용을 보면 메소드 처리 가능여부를 알려주는 respondsToSelector와 메소드의 시그니처(보통반환형식)를 조립하여 반환하는 methodSignatureForSelector: 가 인스턴스 content에도 적용되도록 오버라이드 하고 있다. 

 

 

메시지 포워딩을 통한 다중상속 구현

메시지 포워딩을 이용하면 obj c 한계인 단일 상속을 넘어선 다중상속이 가능한 것처럼 보이게 할 수 있다. 즉 여러개의 클래스 인스턴스를 포함시켜 메시지를 각각의 구성연관 객체에 수행가능한지 점검하고 포워딩하면 마치 두개 이상의 클래스로 부터 상속 받는 다중상속 클래스와 같은 효과를 볼 수 있다. 

 

물론 이런 방법도 다중 상속에서 발생하는 모호성 문제를 동일하게 유발한다. NSNumber와 NSString 모두 동일한 상위계층인 NSObject를 가지고 있다. 만일 아래 그림과 같이 클래스 C가 구성되었을 때, 구성연관 객체 str로 먼저 메시지 포워딩한다고 가정하면 2번 num으로 접근하는 NSObject의 메소드에는 접근이 불가능해진다. 즉, 완전한 다중 상속의 구현은 불가능 하다. 

 

 

예제

 

MyString.h

 

#import <Foundation/Foundation.h>

@interface MyString : NSObject
{
    //구성연관을 위한 멤버변수, 메시지 전송을 통해 수퍼클래스처럼 활용된다.
	NSString *content;
}
-(id)initWithString:(NSString *)string;

//자체처리되는 시험용 메소드
-(NSString *)doMessage;
@end

 

MyString.m

 

#import "MyString.h"

@implementation MyString

//실제로 초기화가 완료되어 content인스턴스가 배정되기 전까지 메시지 포워딩이 동작하지 않는다.
-(id)initWithString:(NSString *)string
{
	self = [super init];
	if (self)
        NSLog(@"initWithString - 초기화 : %@" , [string retain]);
		content = [string retain];
    
	return self;
}
-(void)dealloc
{
	[content release];
	[super dealloc];
}
-(NSString *)doMessage
{
	return content;
}



//아래는 메시지 포워딩을 위한 관용적인 표현

-(void)forwardInvocation:(NSInvocation *)anInvocation
{
    
    NSLog(@"forwardInvocation");
    
	SEL sel = [anInvocation selector];
    if([content respondsToSelector:sel]){
        
        NSLog(@"forwardInvocation - 1");
        
		[anInvocation invokeWithTarget:content];
        
    }else{
        
        NSLog(@"forwardInvocation - 2");
        [super forwardInvocation:anInvocation];
        
    }
}
-(BOOL)respondsToSelector:(SEL)aSelector
{
    
    NSLog(@"respondsToSelector");
    
    if([self methodForSelector:aSelector] != (IMP)NULL){
        
        NSLog(@"respondsToSelector - 1");
        
        return YES;
        
    }
    
    
    if([content respondsToSelector:aSelector]) {
        
        
         NSLog(@"respondsToSelector - 2");
        
        return YES;
        
    }
    
    if([super respondsToSelector:aSelector]){
        
        
        NSLog(@"respondsToSelector - 3");
        
        return YES;
    }
    return NO;
}
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    
    NSLog(@"methodSignatureForSelector");
    
    if([content respondsToSelector:aSelector]){
        
        NSLog(@"methodSignatureForSelector - 1");
        
		return [content methodSignatureForSelector:aSelector];
    }
    
	return [super methodSignatureForSelector:aSelector];
}
@end

 

main.m

 

#import <Foundation/Foundation.h>
#import "MyString.h"

int main(int argc, const char * argv[])
{
	@autoreleasepool
	{
		id a = [[MyString alloc] initWithString:@"Cho Y. S."];
		printf("%s\n", [[a doMessage] UTF8String]);
		printf("%d\n", (int)[a length]);
	}
    return 0;
}

 

결과

 

initWithString - 초기화 : Cho Y. S.
Cho Y. S.
methodSignatureForSelector
methodSignatureForSelector - 1
forwardInvocation
forwardInvocation - 1
9

 

 

메시지 포워딩의 또 다른 문제

메시지 포워딩이 서브클래스 방식에 비해 구현이 안되는 또 다른 문제점은 초기화 메소드의 상속이다. 위의 예제를 보면 초기화 수단으로 initWithString 메소드만 구현되어 있다. 이 상태에서는 NSString 클래스에서 제공하는 다른 초기화 메소드인 initWithUTF8String : 등과 같은 간편생성자에는 동작하지 못한다.

 

이런 현상이 발생하는 이유는 인스턴스 변수 content가 설정완료되기 전까지는 메소드 포워딩에 응답 못하기 때문이다. 즉, 모든 초기화 메소드로 인스턴스를 생성하기 원하면 위와 같이 각 초기화에 대응하는 메소드를 모두 구현 해야 한다. 

 

 

클래스 클러스터를 상속받는 서브클래스를 만드는 마지막 방법

클래스 클러스터로 구성된 클래스는 원시 메소드(프리미티브 메소드)를 기반으로 구성된다. 즉 원시 메소드 이외의 메소드는 반드시 원시메소드를 통해서만 동작하게 된다.

 

 

위의 그림과 같이 NSString의 원시메소드는 두개만 존재한다. 이제 NSString의 상속을 받는 클래스를 작성하기 위해서는 이 두개의 메소드를 자신의 목적에 맞게 오버라이드한다. 원시메소드의 동작이 사용자의 요구와 같더라도 재정의 해야 한다. 왜냐면 가성 클래스 상태에서 원시 메소드는 미정의 상태이기 때문이다. 여기에 추가로 사용자 목적에 맞는 추가 메소드 또는 멤버변수를 정의하면 된다. 물론 활용 메소드 전체를 재정의할 필요는 없다. 클래스 클러스터의 서브클래스를 만드는 단계를 정의하면 다음과 같다.

 

 

사용자 서브클래스의 멤버변수를 선언한다. (상위클래스의 데이터를 상속받아 사용할 수 없기 때문이다.)

 

초기화 메소드와 간편 생성자를 정의한다. 이 역시 상위클래스의 자료 접근이 불가하기 때문에 사용할 수 없다. 다만 인자가 없는 init 메소드만 그대로 사용할 수 있다.

 

원시메소드를 정의한다. 원시메소드의 정의가 완료되면 상위 공개클래스에서 정의한 메소드는 원시메소드에 따라 동작한다. 요구에 따라 이 활용 메소드를 오버라이드 할 수 있다. 마지막으로 그외 추가적으로 필요한 메소드를 정의한다.