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

objective c 프로토콜 2 - 프로토콜의 채용, 상속 카테고리화

by 인생여희 2020. 12. 31.

objective c 프로토콜 2 - 프로토콜의 채용, 상속 카테고리화

 

프로토콜은 가상메소드의 모음이며, 다중상속을 위해 태어났기 때문에, 채용되는 프로토콜 개수의 제한은 없다. 이럴 경우, 콤마로 구분하여 채용할 프로토콜을 여러개 나열하면 된다. 

 

@interface ClassWithProtocol : Ancestor <Print, Display>

 

 

 

아래 예에서 클래스 ClassWithProtocol을 선언하는 과정에서 우선 Ancestor라는 클래스를 상속받고 있다. 이 Ancestor는 NSObject를 직접 또는 간접적으로 상속받을 것이다. ClassWithProtocol 클래스는 두개의 프로토콜 <Print>와 <Display>를 상속 받고 있다. 클래스의 상속은 단일 상속만 가능하지만 프로토콜의 상속은 다중상속이 가능하다. 

 

위의 예는 클래스가 다중 프로토콜의 상속을 받는 것을 나타낸다. 이번에는 프로토콜 간의 상속을 알아본다. 프로토콜의 채용과 상속의 표현방법은 같다. < , > 꺽쇠로 표시해주면 된다. 

 

 

 

 

마지막으로 프로토콜을 카테고리에 채용하는 방법을 알아본다. 이렇게 할 경우 장점은 기존 클래스에 변경없이, 프로토콜을 추가할 수 있기 때문이다. 클래스에 채용된 프로토콜 구현은 클래스 구현파일 .m에서 하지만, 카테고리에 채용될 경우 카테고리 구현파일에서 가상함수를 구현한다. 즉, 클래스 본체에는 변경을 가하지 않고 프로토콜을 채용할 수 있게 된다. 

 

 

 

위 같은 방법을 이용하면 기존에 사용하던 클래스의 변경 없이 프로토콜을 적용할 수 있게 된다. 여기서 기존에 사용하던 클래스란 기초 프레임워크에서 제공하는 클래스 또는 개인이 설계한 클래스 모두를 뜻한다. 심지어 NSObject에도 개인이 설계한 프로토콜을 채용하여 기존 코드에 변경 없이 프로토콜 메소드를 호출할 수 있다. 단, 이런 경우라도 위의 카테고리 선언파일은 반드시 #import 하여야 카테고리(프로토콜)의 추가 메소드를 사용할 수 있다.

 

프로토콜의 카테고리화는 본래 내부적으로 동일한 방법으로 구현되는 메소드 함수의 확장을 외부적으로 그 표현 방법만 바꾸는 것에 불과하다. 종종 프로그램을 설계하는 도중에 카테고리 확장방법과 프로토콜 도입방법의 차이, 장단점 또는 설계 시 고려사항 등을 결정하기 어려워하는 경우가 있으나 사실 크게 고민 할 필요는 없다. 두 방법은 완전히 동일한 것이며 프로그램적으로 그 표현법이 조금 다를 뿐이다.

 

예제

 

Print.h

 

#import <Foundation/Foundation.h>

//프로토콜 선언
@protocol Print
-(NSString *)methodA;
-(NSString *)methodB;
@end

 

 

NSObject+Dummy.h

 

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

//카테고리의 프로토콜 채용
//이 경우 카테고리는 별도의 카테고리 메소드 추가 없이
//프로토콜의 메소드만 변환
@interface NSObject (Dummy) <Print>
@end

 

 

NSObject+Dummy.m

 

#import "NSObject+Dummy.h"

@implementation NSObject (Dummy)

//메소드 정의/구현
//이 메소드는 카테고리 메소드이면서,
//프로토콜 메소드이다.

-(NSString *)methodA
{
	return @"methodA from (Category) <Print>.";
}
-(NSString *)methodB
{
	return @"methodB from (Category) <Print>.";
}
@end

 

main.m

 

#import <Foundation/Foundation.h>
#import "NSObject+Dummy.h"

int main(int argc, const char * argv[])
{
	@autoreleasepool
	{
		id a;
		a = [[NSObject alloc] init];
        
        //응답결과
        //카테고리화 되어 있지만 프로토콜 Print 채용
		printf("%d\n",				// 1
			   (int)[a conformsToProtocol:@protocol(Print)]);
		
        
        printf("%d %d\n",			// 1 1
			   (int)[a respondsToSelector:@selector(methodA)],
			   (int)[a respondsToSelector:@selector(methodB)]);
		
        
        printf("%s %s\n",
			   [[a methodA] UTF8String],
			   [[a methodB] UTF8String]);
		[a release];
	}
    return 0;
}

 

결과

 

1
1 1
methodA from (Category) <Print>. methodB from (Category) <Print>.
Program ended with exit code: 0

 

0502-protocol category.zip
0.10MB

 

 

프로토콜 수행 여부를 검사

 

각 변수들은 자신의 형에 포함된 상위계층의 메소드 호출이 가능하고, 하위 계층의 메소드 호출은 경고를 유발한다고 했다. 대신 id 형식 변수는 모든 메소드를 자유롭게 호출할 수 있다. 추가로 계층 계보에 따라서 변수형의 캐스팅도 가능하다.

 

그렇다면 특정 프로토콜을 수행할 수 있는 변수 형식을 표현하는데 기존의 클래스 형식 변수와 id 형식이면 충분하나? 아니다. 프로토콜은 다중상속과 여러 클래스에 적용 가능하기 때문에 클래스 계층에 의존한 변수 형식과는 무관하게 동작한다. 

 

예를 들어, Print 프로토콜을 수행할 수 있는 변수를 클래스 형식으로 표현하는 것은 현실적으로 불가능하다. 그렇다고 프로토콜 수행 여부를 컴파일 기간에 점검하지 않고 단순히 모두 id 형식으로 관리하는 것은 찾기 어려운 실행시간 오류를 유발 할 수 있다. 

 

 

 

 

안전한 id 관리를 위한 방법

 

이전 포스팅 예제에서 메인 함수 id형 변수에 <Print>라고 기재해 주면 해당 변수는 <Print> 프로토콜을 채용하여야 한다는 조건이 만들어 진다. 만일 이 변수에 프로토콜을 채용한 객체(ClassWithProtocol)가 할당되면 문제가 없으나 프로토콜을 채용하지 않는 객체 (Ancestor, ClassWithoutProtocol)가 할당되면 경고가 발생한다. ClassWithOutProtocol은 <Print> 프로토콜의 메소드를 모두 가지고 있지만 명시적으로 채용하지 않았기 때문에 이 역시 경고가 발생한다.