[Objective-C] 프로토콜

Objective-C의 프로토콜

프로토콜의 개념 잡기

프로토콜은 “선언만 되고 구현되지 않은” 메소드를 말한다. 이 프로토콜은 역시나 “너무 단순해서 쉽게 감이 잡히지 않는” Objective-C의 기능이다. 프로토콜은 크게 다음 3가지 경우에 유용하게 사용된다.

  1. 다른 객체가 구현해주면 되는 메소드를 선언
  2. 클래스를 숨기고 인터페이스만 선언하고자 할 때
  3. 상속관계가 아니지만 비슷한 인터페이스를 만들고자 할 때

어떤 객체가 그 자신은 구현하지 않지만 델리게이트에게 위임하여 실행할 어떤 기능이 필요하다고 하면 프로토콜로 선언하고, 델리게이트가 그 프로토콜을 따르도록하여 실제 기능의 실행을 델리게이트에서 처리하도록 할 때 프로토콜이 쓰이는데, 그 흔한 예는 델리게이트이다.

예를 들어 A라는 클래스에서 어떤 동작을 수행하고 나서 델리게이트를 통해서 B라는 동작을 수행하게 하려는 상황을 가정해보자. 이때 확정되는 것은 B라는 동작이 정의된 메소드 이름 뿐이다. 물론 respondsTo: 를 이용해서 특정한 메소드를 가지고 있는지를 검사하는 것은 가능하겠지만, 그 보다는 이 동작을 델리게이트 프로토콜로 정의하고 델리게이트가 그 프로토콜을 따르도록 하는 것이 더 매끈한 디자인이 될 것이다.
여기서 실제 메소드의 선언을 해당 객체가 해주고, 다른 객체는 구현만한다는 측면에서 2, 3의 경우는 이해할 수 있을 것이다.

다른 객체가 구현해줄 인터페이스 선언하기

클래스 인터페이스나 카테고리는 특정한 클래스가 구현해야 하는 기능을 선언해준다. 프로토콜은 이와 반대로 (알 수 없을지도 모르는) 다른 객체가 구현해 줄 기능을 선언하는 것이다. 즉, 동작을 선언하지만, 이 동작을 수행할 객체의 타입은 결정할 수 없거나 타입에 상관없이 메시지를 받을 수 있어야 할 때 사용하게 된다.

따라서 프로토콜은 단순히 메소드 선언의 묶음만 으로 구성된다. 예를 들어 어떤 마우스 이벤트를 받아 이를 처리해야 할 때, 대충 다음과 같은 모양의 메서드들이 필요할 수 있다.

  • -(void)mouseDown:(NSEvent *) theEvent;
  • -(void)mouseDragged:(NSEvent *) theEvent;
  • -(void)mouseUp:(NSEvent *) theEvent;

이 메소드를 해당 객체에서 직접 구현해도 되지만, 다른 객체가 그 작업을 대신 받아서 하도록 하려면 이를 프로토콜로 선언하고, 다른 객체가 그 프로토콜을 따르도록 하면 되는 것이다.

프로토콜은 이를 통해서 막대한 유연성을 코드에 부여할 수 있다. 즉 인터페이스의 선언을 클래스의 프로토타입과 완전희 분리하는 것이다. 프로토콜에서 선언된 메소드들은 어딘가에서 구현되겠지만, 이를 구현할 메소드의 클래스는 무엇이어도 상관없다.

따라서 전혀 다른 클래스 들이라도 같은 프로토콜을 따른다면 같은 메소드를 가지고 있다고 생각할 수 있다1. 이때, 같은 프로토콜을 따르는 클래스들은 상속관계에서 서로 관련이 없다고 하더라도 기능적인 측면으로 분류할 때 유사한 그룹으로 묶을 수 있다.

다른 객체가 구현할 메소드

앞서 언급했지만 다른 객체에게 작업을 위임하거나 (델리게이트) 다른 객체로부터 정보를 받아와야 하는 경우 (데이터소스)에는 보통 해당 객체에게 메시지를 보내어 이를 처리하면 된다.

코드 레벨에서 생각해본다면, 통상 메시지를 주고 받는 형태에서는 메시지를 보내는 객체는 이 메시지를 받을 객체의 헤더 파일을 임포트하여 수신 객체에게 보낼 메시지의 셀렉터를 미리 알고 있어야 한다.2

하지만 이를 수신할 객체가 아직 만들어지지 않은 상황이라면, 인터페이스 파일을 임포트할 수가 없다. 따라서 이 경우에는 아직 만들어지지 않은 객체가 받을 메시지를 정할 수 없게된다. 이 때 프로토콜을 사용하면 수신 객체의 타입이 뭐든 간에 프로토콜을 따르고 있다는 전제가 있기에, 수신 객체의 헤더를 임포트하지 않고서도 수신 객체에게 메시지를 보낼 수 있게 된다.

만약 어떤 객체에게 helpOut: 이라는 메시지를 보내어 처리할 일이 있다고 한다면 다음과 같이 처리하게 될 것이다.

-setAssistant:anObject
{
    assistant = anObject;
}

만약 이 객체를 assistant라는 프로퍼티라 생각한다면,

-setAssistant:anObject
{
    assistant = anObject;
}

-(BOOL)doWork
{
    ...
        if ( [self.assistant respondToSelector:@selector(helpOut:)]) {
            [self.assistant helpOut:self];
            return YES;
        }
    return NO;
}

위와 같이 해당 객체가 helpOut: 이라는 메시지에 응답할 수 있는지 확인한 다음 메시지를 보내게 된다. 하지만 이 객체가 특정한 프로토콜을 따른다면 이러한 검사 절차 없이 바로 호출할 수 있다.

앞서도 언급했지만, 이것의 단적인 예가 델리게이트이다. 보통 기본적으로 우리가 작성하는 코드에서는 델리게이트 입장에서 코드를 쓴다. 대표적으로 테이블 뷰 컨트롤러가 그 예이다. 데이터소스나 델리게이트 프로토콜을 따르는 객체를 만들어 그 프로토콜의 필수 메소드를 구현해주고 있는 것이다. 반대로 델리게이트에게 일을 시키는 객체에서는 다음과 같은 코드들을 볼 수 있다. 먼저 헤더 파일에서는 프로토콜을 선언하고, 그 프로토콜을 따를 델리게이트를 선언한다.

// SomeClassNeedsDelegate.h
import <Foundation/Foundation.h>

@protocol MyClassDelegateProtocol
-(void)helpOut:(id)sender
@end

@interface SomeClassNeedsDelegate : NSObject
@property (nonatomic, weak) id<MyClassDelegateProtocol> delegate;
@end

구현 부분에서는 델리게이트에게 메시지를 보낼 수 있다. 프로퍼티로 설정한 델리게이트는 만약 이 객체를 생성하거나 참조하는 쪽에서 델리게이트를 지정할 것이고, (하지 않았다면 nil) 따라서 바로 메시지를 보내도 문제는 없다.

[self.delegate helpOut:self]

델리게이트가 되는 객체는 인터페이스를 선언할 때 (내부든 외부든 무관하다) 이 프로토콜이 선언된 헤더를 임포트하고 인터페이스 시작 지점에 다음과 같이 쓴다.

// another object that will be the delegate of 'SomeClassNeedsDelegate'
import "SomeClassNeedsDelegate.h"

@interface MyAnotherClass : NSObject <MyClassDelegateProtocol>
<# 클래스 인터페이스 선언 #>

이와 관련한 자세한 내용은 다음 절에서 설명하겠다.

프로토콜 정의 방법

프로토콜은 헤더파일의 인터페이스 영역과 별개의 영역을 만들어 메소드의 선언을 하게 된다.

@protocol 프로토콜이름 : 상속받을 프로토콜
- 메소드 이름
@end

다음은 프로토콜 선언의 예시이다.

@protocol MyProtocol : NSObject // 프로토콜은 다른 프로토콜을 상속할 수 있다.
-(void)requiredMethod;
@optional
-(void)anOptionalMethod;
-(void)anotherOptionalMethod;
@required
-(void)anotherRequiredMethod;
@end;

@optional 키워드 다음에 나오는 메소드들은 모두 선택적으로 구현하면 되는 메소드들이며, 이런 키워드가 없는 경우에는 모두 필수적으로 구현해야 하는 메소드로 인식한다. @optional 키워드로 선언한 메소드들 뒤에 다시 필수 메소드를 추가로 선언하려면 @required 키워드를 다시 써주면 된다.

프로토콜 따르기

객체가 어떤 프로토콜을 따른다고 할 때에는 다음과 같은 절차를 따른다.

  1. 프로토콜이 선언된 인터페이스 파일을 임포트한다. 이 파일은 프로토콜에 선언된 메소드를 필요로하는 객체의 헤더 파일인 경우가 많으나, 별도의 인터페이스 파일에 프로토콜을 선언할 수도 있다. 많은 프로토콜이 혼재하는 프로젝트의 경우라면, 별도의 프로토콜 인터페이스 파일을 따로 설정하는 것이 편리할 수 있다.
  2. 인터페이스 블럭 시작줄에 <프로토콜이름> 이라고 꺾은 괄호로 둘러싸고 프로토콜 이름을 명시해준다.
  3. 해당 프로토콜에서 필수로 선언한 메소드들을 구현한다.

Formatter라는 클래스가 Formatting, Prettifying 이라는 두 개의 각각 다른 프로토콜을 따른다고 할 때 다음과 같이 인터페이스를 작성하면 된다.

@interface Formatter : NSObject <Formatting, Prettifying>
<# 인터페이스 선언 #>
@end

또한, 객체를 선언할 때 그 객체가 특정 프로토콜을 따르고 있음을 명시할 때 (프로토콜을 따른 객체를 아직 작성하지 않아서 모를 때 사용함) 다음과 같이 타입 명 뒤에 프로토콜 명을 사용할 수 있다.

Formatter <Formatting> *aFormatterObject;
id<Prettifying> *aPrettifier;

비정규 프로토콜

비정규 프로토콜은 카테고리의 형식으로 프로토콜을 대체하는 방법이다. 코코아의 거의 모든 객체들은 NSObject를 상속받으므로, NSObject의 카테고리로 메소드들을 선언하면 별도의 프로토콜을 따른다는 명시 없이도 해당 메소드를 선언한 것과 같은 효과를 낼 수 있다. 이 방법은 모든 메소드가 선택적일 때 사용하면 명시적인 코드를 조금 더 줄일 수 있는 효과가 있다.

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

만약 어떤 프로토콜 A에서 사용하는 타입이 B라는 프로토콜을 따라야 하는데, 이 B에 대한 프로토콜이 아직 작성되지 않았다면 프로토콜이 선언된 인터페이스 파일조차 임포트하지 못하는 경우가 있다.

이럴 때는 @class ClassName; 과 마찬가지로 @protocol someProtocolName; 과 같이 이런 프로토콜이 있을 것이라고 컴파일러에게 알려줄 수 있다. 이는 두 개의 프로토콜이 서로를 동시에 참조하는 경우에 사용하여 컴파일러가 에러를 내지 않도록 할 수 있다.


  1. 같은 프로토콜을 따른 다는 것은 프로토콜에 정의한 메소드를 구현해두었다는 것이므로 
  2. 항상 그런 것은 아니다. Objective-C는 동적 타입 언어이므로 [RespondsTo:] 메시지를 이용하여 먼저 메시지를 받을 수 있는지 검사할 수도 있다. 하지만 매번 그러려면 너무 귀찮고, IDE가 제공하는 기능들을 활용할 수 없는 불편을 겪어야 한다. 
  • Ji_dung

    검색을 통해 들어왔는데 굉장한 강의네요. 감사합니다

  • KEA

    유익한 정보 잘 보고 갑니다.^-^
    Objective-C과정 진행하는 곳이 있어서 소개 드립니다.
    5/13~5/15 서울상암에서 진행하며, 자세한 내용을 URL을 참조해주세요~
    http://educ.or.kr/plato/?mode=info&did=14&uid=340&special=N

  • 님짱

    정말 도움 많이 되었습니다. 글 굉장히 잘 쓰시네요. ^^

  • Jung

    잘 보고 갑니다.