Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Objective-C

21. Objective-C Protocol 프로토콜

by Toughie 2024. 5. 22.

KAKAO-Choonsik

 

[프로토콜]

 

프로토콜은 '이러한 메서드를 구현해야 해!'라는 제약사항이라고 설명할 수 있다.

 

Objective-C에서 상속은 단일 상속만 지원하는 반면, 프로토콜은 다중 채택이 가능하다.

또한 프로토콜을 통해 객체를 참조할 수도 있기 때문에 다형성 구현에도 매우 유용하다.

즉 클래스 단위에 의존하는 대신, 프로토콜에 의존하도록 객체 간 결합도를 낮출 수 있다는 것이다.


프로토콜 문법

@protocol 프로토콜이름
@required
//반드시 구현해야 하는 메서드
@optional
//구현이 선택인 메서드
@end

 

프로토콜 채택(준수)

@interface MyClass: NSObject <프로토콜1, 프로토콜2>
...
@end

채택(adopting), 준수(conforming)의 미묘한 차이가 있지만

둘 다 클래스가 프로토콜이 정의한 요구 사항을 충족한다는 의미로 사용된다.

(자바에서 인터페이스를 구현한다와 거의 같은 의미임)


프로토콜 예시

#import <Foundation/Foundation.h>

@protocol Shape
@required
-(void) draw;
@optional
-(void) color;
@end

 

 

필수 구현요소로 '도형 그리기', 그리고 선택 구현요소로 '색칠하기'가 포함된 Shape라는 프로토콜을 만들었다.


프로토콜을 채택하는 클래스

@interface Circle: NSObject <Shape>
-(void) draw;
-(void) color;
-(void) onlyCircle;
@end

@implementation Circle
-(void) draw {
    NSLog(@"CIRCLE");
}
-(void) color {
    NSLog(@"COLORED CIRCLE);
}
-(void) onlyCircle {
    NSLog(@"ONLY CIRCLE");
}
@end

 

프로토콜에서 요구되는 필수 메서드인 draw, 선택 메서드인 color, 그리고 자체 메서드인 onlyCircle이 있다.

다형적 참조 에시를 위해 프로토콜만 준수하는 다른 클래스를 하나 더 만들어 보자.

 

@interfacce Rectangle: NSObject <Shape>
-(void) draw;
@end

@implementation Rectangle
-(void) draw {
    NSLog(@"RECTANGLE");
}
@end

객체 생성 및 참조 방법

클래스 인스턴스를 참조할 때 클래스 타입의 포인터만 사용했다면,

이제는 프로토콜을 통해서도 인스턴스르 참조할 수 있다.

객체의 타입에만 의존하지 않아도 되는 유연성이 생긴 것이다.

또한 같은 프로토콜을 준수하는 객체라면 하나의 참조변수가 다양한 객체를 참조할 수 있다.

#import <Foundation/Foundation.h>

int main(void) {
  @autoreleasepool {
    //기본 참조 방식
    Circle *circle1 = [[Circle alloc] init];
    //프로토콜 참조 방식
    id<Shape> circle2 = [[Circle alloc] init];
    //명시적 참조 방식
    Circle<Shape> *circle3 = [[Circle alloc] init];

    [circle1 draw];
    [circle1 color];
    [circle1 onlyCircle];

    [circle2 draw];
    [circle2 color];
    //Instance method not found (return type defaults to 'id')
    // [circle2 onlyCircle];

    [circle3 draw];
    [circle3 color];
    [circle3 onlyCircle];

    //다형적 참조
    id<Shape> random = [[Rectangle alloc] init];
    [random draw]; //RECTANGLE
    random = [[Circle alloc] init];
    [random draw]; //CIRCLE
  }
}

 

위와 같이 Circle의 객체를 참조하는 변수를 만드는 방식이 다양하다.

기본적으로 클래스 타입으로 참조할 수도 있고, 프로토콜을 통해 참조할 수도 있고

클래스와 프로토콜을 전부 명시하는 방식으로 참조할 수도 있다.


id<Protocol>

id는 Objective-C에서 '아무 객체 타입'을 의미하는 특별한 타입이다.

어떤 클래스의 인스턴스든 참조할 수 있다. 

id에 프로토콜을 결합하면 특정 프로토콜을 준수하는 객체를 참조할 수 있다.

 

왜 *를 쓰지 않을까?

id 타입이 이미 포인터이기 때문에 *를 통서 포인터라고 명시할 필요가 없기 때문이다.


메서드 호출

클래스타입으로 참조하는 circle1에서는 onlyCircle메서드의 자동완성 및 호출이 잘 되지만

 

Shape 프로토콜로 참조하는 circle2에서는 onlyCircle이 자동완성도 되지 않고,

instance method not found라는 경고를 띄운다. 

실행도 되지만 사실 이건 올바른 접근이 아니다.

 

접근제어에서 메서드와 유사한 이유인데,

간단하게 말하면 Shape라는 프로토콜에는 draw, color라는 메서드 정보는 있지만

onlycircle이라는 메서드에 대한 정보는 없다.

근데 circle2는 지금 Shape로 참조를 하고 있기 때문에 메서드를 못 찾는다는 경고를 띄우는 것이다.

 

하지만.. Objective-C에서 메서드의 동적 바인딩 및 메서드 디스패치 테이블의 동작 방식으로 인해

이미 객체에는 메서드가 있기 때문에 직접 작성해서 호출해 버리면 호출은 되는 것이다.

하지만 이는 의도되지 않은 동작이기 때문에 ide차원에서 경고를 보내는 것이다. 

 

따라서 호출이 필요한 경우 명시적 타입캐스팅을 해주면 경고 없이 잘 실행할 수 있다.

//explicit typeCasting
[(Circle *) circle2 onlyCircle];

다형적 참조

예시코드의 random 변수를 보면 하나의 변수로 다양한 클래스의 객체를 참조할 수 있음을 알 수 있다.

프로토콜이 없다면 각 클래스 타입에 맞는 변수로 참조를 해야겠지만, 현재 Circle과 Rectangle 전부
Shape라는 프로토콜을 준수하고 있기 때문에 id <Shape> 타입을 통해서 둘 다 참조가 가능한 것이다.

 

현재는 예시코드가 너무 간단하지만, 코드의 볼륨이 커지고 확장성과 수정가능성을 고려해야 하는 경우에

프로토콜을 통한 다형적 참조는 매우 유용하고 중요하다.

 

이 상자에는 사과만 담을 수 있어! 보다는 이 상자에는 과일이기만 하면 담을 수 있어가 훨씬 유연한 것처럼 말이다.

사과가 특정 클래스타입이라면, 과일은 프로토콜이 될 것이다.