콘텐츠로 건너뛰기
Home » [Objective-C] 프로토콜

[Objective-C] 프로토콜

클래스는 서로 다르지만 A라는 똑같은 메소드를 하나씩 가지고 있는 어떤 객체들이 있다고 하자. 방금 A라는 메소드를 가지고 있다고 가정했기 때문에 이 객체들에게는 A라는 메시지를 보냈을 때 해당 메소드가 실행될 것이라는 게 보장된다. 프로토콜은 이처럼 특정한 메소드를 구현해서 갖추고 있겠다는 약속을 말한다.

어떤 객체가 특정한 프로토콜을 따르고 있다면, 그 프로토콜에서 선언된 메소드는 구현하고 있다는 의미이며, 해당 메소드를 호출할 수 있다. 이 때 객체의 클래스가 무엇인지는 관심의 대상이 아니다.

프로토콜은 Objective-C의 기능으로 “선언만 되고 구현되지 않은” 메소드를 말한다. 이 기능은 흔히 ‘다중 상속을 지원하지 못하는 Objective-C의 기능을 보완하기 위해’ 만들어졌다고 하는데, 틀려먹은 이야기이고… Swift에서는 특히 중요하고 광범위하게 사용되기 때문에 주목을 많이 받는데, Objective-C에서는 그렇게 중요하게 다뤄지지 않았던 거 같아서 (아니, 다루는 블로그가 별로 없었지) 정리하도록 하겠다.

어떤 객체의 클래스에 대해서 알고 있다면, 이 클래스가 제공하는 인터페이스를 통해서 우리는 객체가 지원하는 기능들을 사용할 수 있게 된다. 거꾸로 어떤 특정한 기능이 필요하다면 우리는 그러한 기능을 지원하는 클래스를 선택해서 사용해야 한다. 그런데 특정한 기능은 정해져 있지만, 어떤 클래스를 사용하게 될지 알 수 없다면 어떻게 할까?

프로토콜은 특정한 메소드 이름들의 목록이면서, 이러한 메소드를 구현하고 있을 것이라는 약속이라 할 수 있다. 따라서 클래스의 실제타입이 무엇이든 상관없이, 어떤 임의의 클래스가 한 프로토콜을 따른다는 것만 알 수 있다면, 그 클래스의 종류에 상관없이 약속된 메소드를 호출할 수 있다는 것을 보장한다. 따라서 프로토콜은 어떤 클래스가 특정한 인터페이스를 가지고 있을 것이라는 약속으로 이해하면 된다.

이러한 프로토콜은 실제로 코코아에서 광범위하게 사용된다. 특히 **델리게이트나, **데이터소스 와 같은 이름의 프로토콜을 흔히 볼 수 있다. 대표적으로는 UIApplicationDelegate, UITableViewDataSource, UITableViewDelegate 등의 프로토콜이 있다. 테이블 뷰를 예로 들면, 테이블 뷰를 구성하기 위해서는 총 몇 개의 행이 필요한지, 각각의 행에 해당하는 셀은 어떤 객체인지에 대한 정보는 테이블 뷰가 알 수 없는 상태이다.

테이블 뷰에 필요한 정보를 주입하기 위해서 UITableView를 상속받아 새로운 커스텀 클래스를 만들어서 사용하는 방법이 있을 것이지만 테이블 뷰 자체가 복잡하고 거대한 클래스이기 때문에, 테이블 뷰에게 필요한 정보를 제공할 수 있는 클래스를 하나 만들고 테이블 뷰가 해당 클래스의 객체에게 정보를 요청하도록 하는 것이, 코드 관리 측면에서도 잇점이 많을 것이다. 대신에 테이블 뷰가 자신의 데이터 소스에게 정보를 요청하는 메소드는 고정되어야 하므로, 이러한 메소드들의 이름과 시그니처만 미리 정의해두는 것이 UITableViewDataSource 프로토콜이다.

다음은 프로토콜의 사용을 고려해야 하는 경우들이다. 이 중 1번이 데이터소스나 델리게이트에 해당한다.

  1. 다른 객체가 구현해주면 되는 메소드를 선언하고, 구현은 다른 누군가에게 위임하려 할 때.
  2. 구체적인 클래스에는 관심이 없고 인터페이스의 정의만 필요할 때.
  3. 특정 인터페이스를 공통으로 가지고 있지만, 상속 관계는 필요없는 클래스들을 사용하려 할 때.

프로토콜 선언 방법

델리게이트를 사용하는 클래스를 정의하는 과정을 살펴보면서 프로토콜을 어떻게 정의하는지, 그리고 프로토콜을 따르는 클래스를 어떻게 사용하는지를 살펴보자. 델리게이트 패턴을 사용하는 가장 일반적인 케이스는 이벤트 및 콜백에 대한 처리를 위임하는 것이다. 예를 들어 인터넷에서 데이터를 다운로드하는 클래스를 작성한다고 하자. 이 클래스는 2가지의 다운로드 메소드를 제공할 수 있는데, 하나는 동기식으로 데이터 다운로드가 완료될 때까지 다른 코드가 실행되지 않고 블럭될 것이며, 다른 하나는 비동기 방식으로 백그라운드에서 다운로드를 할 것이다.

백그라운드에서 다운로드하는 메소드는 호출 즉시 리턴하게 될 것이므로, 수신한 데이터를 나중에 사용하기 위한 장치가 필요한데 이를 위해서 델리게이트를 하나 정의하게 되는 것이다. 예시로는 성공했을 때 NSData 객체를 받는 메소드와, 실패했을 때, HTTP 에러 정보를 담는 NSDictionary 객체를 전달받는 두 개의 델리게이트 메소드를 미리 정의해 둔다.

/* @class 는 MyDownLoader의 헤더를 임포트하지 않더라도
   해당 클래스를 미리 알고 있는 것처럼 가정하여
   불필요한 에러나 경고를 무시하는 역할을 한다.
*/
@class MyDownLoader

@protocol MyDownLoaderDelegate
-(void) downLoader:(MyDownLoader *)downloader didFinishedDownlaodingData:(NSData *)data;
@optional
-(void) downLoader:(MyDownLoader *)downloader didFailedToDownlaod:(NSDictionary *)errorInfo;
/** 만약 뒤쪽에서 필수 메소드를 추가로 선언하고 싶다면
    @required 지시어를 사용해서 구역을 다시 추가할 수 있다.
**/
@end

통상 델리게이트 프로토콜을 정의할 때, 해당 프로토콜을 준수하는 객체는 하나의 객체가 여러 객체에서 발생한 이벤트를 같이 처리해 줄 수도 있기 때문에, 콜백을 호출하는 객체를 인자로 넘겨받는 경우가 많은데 이 예제 역시 이러한 관례를 따르는 것으로 했다.

프로토콜의 선언 방식은 클래스 인터페이스 선언과 유사하다. 프로토콜은 다른 프로토콜을 상속 받을 수 있으며, 여러 프로토콜을 동시에 상속할 수도 있다. 인터페이스 중에서 선택적으로 구현하면 되는 것들은 @optional 이라고 섹션을 분리하여 선언해주면 된다. (선택적 인터페이스가 필요없다면 생략할 수 있다.)

프로토콜은 인터페이스 선언을 클래스의 프로토타입과 완전히 분리하여, 코드에 큰 유연성을 부여한다. 따라서 전혀 다른 클래스 들이라도 같은 프로토콜을 따른다면 같은 메소드를 가지고 있는 것이 보장되며, 이 클래스들은 상속으로 관련되지 않더라도 같은 부류로 취급할 수도 있다. (따라서 프로토콜 자체를 하나의 타입이나 타입군으로 보는 관점도 타당하다.)

프로토콜을 따르는 클래스 작성하기

클래스 인터페이스를 작성할 때, 특정 프로토콜을 따르는 것을 명시하기 위해서는 부모 클래스 뒤에 < ... > 꺾쇠 괄호 내에 프로토콜을 명시해준다. 하나의 클래스가 여러 프로토콜을 동시에 준수하는 것도 가능하며, 복수 개의 프로토콜을 따르려 하는 경우에는 각 프로토콜의 이름을 콤마로 구분하면 된다.

@interface FileSaver: NSObject <MyDownLoaderDelegate>
@end

@implementaton FileSaver 

-(void) downLoader:(MyDownLoader *)downloader didFinishedDownlaodingData:(NSData *)data
{
    /*...
       데이터를 가지고 뭔가 한다...
    ...*/
}

당연히, 프로토콜에서 선언하고 있는 메소드는 중복해서 선언할 필요가 없다.

프로토콜을 따르는 객체를 사용하기

델레게이트 패턴을 사용하는 케이스를 예시로 MyDownloader 클래스에서는 어떻게 델리게이트를 사용하는지 알아보자. 델리게이트는 보통 옵셔널하게 사용되기 때문에 weak 프로퍼티로 선언한다. 그리고 특정한 콜백을 처리하는 시점에 해당 메소드를 호출하면 된다.

먼저 특정한 프로퍼티가 MyDownloaderDelegate 프로토콜을 따른다는 제약조건이 있는 경우, 객체 타입 뒤에 <MyDownloaderDelegate> 라는 프로토콜을 명시해준다.

@interface MyDownLoader
@property (nonatomic, weak) id<MyDownloaderDelegate> delegate;
....
@end

그리고 이를 사용하는 부분은 다음과 같다. 필수 메소드는 구현하고 있다고 가정한 상태로 호출하면 된다. 옵셔널한 메소드의 경우에는 구현하고 있지 않을 수 있으므로 -respondsToSelector: 메소드를 사용해서 우선 사용가능한 메소드인지 확인한 후에 호출한다.

-(void) downloadDataForURL:(NSURL *)fileURL
{
    NSMutableData *data = [NSMutableData data];
    NSError *error = nil;
    /***  
        어찌어찌 데이터를 다운로드 받아서 data 에 붙여둠
    ****/
    if (error == nil) {
        [[self.delegate] downLoader:self didFinishDownloadingData:(NSData*)data];
    } 
    // 에러가 발생한 경우에는 실패시 메소드를 호출한다.
    // 이 메소드는 필수로 구현해야 하는 것은 아니므로, 메시지를 받을 수 있는지 확인해야 한다.
    else if ([self.delegate] respondsToSelector:@selector(downloader:didFailToDownloadData:)]) {
        [[self.delegate] downloader:self didFailToDownloadData:errorInfo];
    }
}

번외 – 비정규 프로토콜

비정규 프로토콜은 카테고리의 형식으로 프로토콜을 대체하는 방법이다. 카테고리를 이미 정의된 클래스를 상속하지 않고 확장하는 Objective-C의 기능으로 Swift의 extension 구문과 비슷하다고 할 수 있는데, 사실은 아주 커다란 클래스에 대해서 특정한 기능들을 묶어서 별개의 소스에 구현하는 기능이라고 생각하면 된다. NSProxy를 제외한 코코아의 모든 클래스들은 NSObject 를 상속하게 되어 있다. 따라서 프로젝트에서 모든 객체가 기본적으로 따라야 하는 프로토콜이 있다면 NSObject+MyCategory 와 같은 식으로 NSObject를 확장하여 모든 클래스가 특정한 메소드를 기본적으로 구현하고 있는 효과를 낼 수 있다. 이것이 비정규 프로토콜이다. 키-밸류 코딩과 키-밸류 옵저빙은 실제로 비정규 프로토콜로서 사용되고 있다.

아직 선언되지 않은 프로토콜

앞서 델리게이트에서 프로토콜을 따르는 코드를 작성할 때, 이 델리게이트를 요구하는 클래스의 헤더를 알 수 없거나 임포트하지 않은 경우에는 @class MyDownloader 라는 구문을 사용해서 해당 클래스를 알고 있는 것처럼 가정하는 코드를 작성할 수 있다고 했다.

반대로 제3자가 정의하기로한 프로토콜이 있다고 약속했지만, 프로토콜이 정의된 헤더를 임포트할 수 없을 때에는 @protocol 프로토콜이름; 으로만 언급하여 이런 프로토콜이 있더라…는 힌트만 컴파일러에게 제공할 수 있다. 이는 보통 두 개의 헤더 파일에서 각각 프로토콜을 정의하는 경우, 한쪽만 다른 한쪽을 include 하려할 때 사용하기도 한다.