-
프로토콜 지향언어, Swift (3)iOS/Swift 2020. 3. 21. 09:11
프로토콜의 상속
프로토콜은 클래스처럼 상속을 통해 정의된 프로퍼티나 메소드, 그리고 초기화 블록의 선언을 다른 프로토콜에 물려줄 수 있다.
하지만 프로토콜은 클래스와 다르게 다중 상속이 가능하다. 즉 여러 개의 프로토콜을 하나의 프로토콜에 한꺼번에 상속하여 각 프로토콜들의 명세를 하나의 프로토콜에 담을 수 있다.
protocol A { func doA() } protocol B { func doB() } protocol C: A, B { func doC() } class ABC: C { func doA() { } func doB() { } func doC() { } }
위의 코드에서 프로토콜 C는 프로토콜 A와 B를 상속받았기 때문에 C를 구현하는 클래스 ABC에서는 프로토콜 A, B, C를 모두 구현해야 한다.
let abc: C = ABC() abc.doA() abc.doB() abc.doC() let a: A = ABC() a.doA() let ab: A & B = ABC() ab.doA() ab.doB() let abc2: A & B & C = ABC() abc2.doA() abc2.doB() abc2.doC()
클래스 ABC는 위와 같은 타입의 변수/상수에 할당될 수 있다. 선언된 타입에 따라서 사용할 수 있는 메소드의 범위는 제한된다.
func foo(abc: C) { } foo(abc: ABC()) func boo(abc: A & B) { } boo(abc: ABC())
클래스 ABC는 위와 같이 다음 타입으로 선언된 함수나 메소드의 인자값으로 할당될 수 있다.
이처럼 상속으로 구성된 프로토콜은 상위 프로토콜에 대한 기능들을 고스란히 가지고 있으므로 상위 프로토콜 타입으로 선언된 변수/상수나 함수의 인자값으로 사용될 수 있다. 또한, 프로토콜을 상속할 때 부모 프로토콜에서 선언된 자식 프로토콜에서의 선언이 겹치더라도 클래스에서처럼 override 키워드를 붙여야 하는 제약이 없다.
상속 관계가 성립된 프로토콜은 is, as와 같은 타입 연산자들을 사용해 타입에 대한 비교와 타입 변환을 할 수 있다.
is 연산자는 주어진 객체를 비교 대상 타입과 비교하여 그 결과를 반환하는데 이때 선언된 변수나 상수의 타입이 아니라 할당된 실제 객체의 인스턴스를 기준으로 비교한다. 할당된 객체가 비교 대상 타입꽈 같거나 비교 대상 타입을 상속받았을 경우 모두 true를 반환하고, 이외에는 false를 반환한다.
as 연산자의 사용법도 클래스에서 타입 캐스팅과 같다. 객체와 비교 대상과의 타입 비교를 위주로 하는 is 연산자와는 달리 as 연산자는 제한된 범위 내에서 타입을 캐스팅할 수 있도록 해준다.
제한된 범위는 다음과 같다.
- 실제로 할당된 인스턴스 타입
- 인스턴스가 구현한 프로토콜 타입
- 클래스가 상속을 받았을 경우 모든 상위 클래스
- 프로토콜 타입이 상속을 받았을 경우 모든 상위 프로토콜
인스턴스 객체를 할당한 변수나 상수가 있을 때, 이 변수나 상수가 선언된 타입보다 상위 타입으로 캐스팅하는 것은 문제가 되지 않으므로 일반 캐스팅 연산자인 as를 사용하여 안전하게 캐스팅할 수 있지만, 선언된 타입보다 하위 타입으로 캐스팅할 때는 주의해야 한다.
실제로 할당된 인스턴스 객체에 따라 캐스팅이 성공할 수도, 실패할 수도 있다.
실제로 할당된 인스턴스 객체의 타입을 기준으로 일치하거나 상위 타입이면 캐스팅이 잘 되겠지만, 그렇지 않으면 캐스팅에 실패한다. 이는 캐스팅 결과값으로 nil이 반환될 수도 있다는 뜻이다.
이 때문에 하위 캐스팅에서는 일반 캐스팅 연산자를 사용하는 대신 옵셔널 타입으로 캐스팅 결과를 반환하는 옵셔널 캐스팅(=as?) 연산자와 캐스팅 실패 가능성을 감안하고서도 일반 타입으로 캐스팅하는 강제 캐스팅(=as!) 연산자 중에서 선택해서 사용해야 한다.
protocol Machine { func join() } protocol Wheel: Machine { func lotate() init(name: String, currentSpeed: Double) } class Vehicle { var currentSpeed = 0.0 var name = "" init(name: String, currentSpeed: Double) { self.name = name self.currentSpeed = currentSpeed } } class Car: Vehicle, Wheel { required override init(name: String, currentSpeed: Double = 0.0) { super.init(name: name, currentSpeed: currentSpeed) } func join() { // join parts } func lotate() { print("\(self.name)의 바퀴가 회전합니다.") } } class Carpet: Vehicle, Machine { func join() { // join parts } } var translist = [Vehicle]() translist.append(Car(name: "자동차", currentSpeed: 10.0)) translist.append(Carpet(name: "양탄자", currentSpeed: 15.0)) for trans in translist { if let obj = trans as? Wheel { obj.lotate() } else { print("\(trans.name)의 하위 타입 변환이 실패했습니다.") } }
위의 예제에서 Car 클래스는 내부에 Vehicle, Wheel, Machine을 모두 상속/구현하고 있으므로 각 타입으로 모두 캐스팅이 가능하다.
하지만 Carpet 클래스는 Vehicle, Machine만을 상속/구현하고 있다.변수 translist는 Vehicle 타입의 모든 객체를 저장할 수 있도록 정의된 배열 객체이다. 실질적으로 타입이 어떤 것이든 Vehicle 클래스를 상속받은 객체라면 모두 translist에 담을 수 있다. (Car, Carpet은 모두 Vehicle을 상속받고 있다.)
translist 배열을 for~in 구문에 넣어서 Wheel 프로토콜로 옵셔널 캐스팅을 했을 때 Car 클래스는 Wheel 타입으로 캐스팅에 성공하지만, Wheel 프로토콜을 구현하지 않은 Carpet은 캐스팅에 실패한다.
클래스 전용 프로토콜
프로토콜은 문법적으로 구조체에서 확장체에 이르기까지 광범위한 객체들이 구현할 수 있지만, 때로는 클래스만 구현할 수 있도록 제한된 프로토콜을 정의해야 할 때가 있다. 이를 클래스 전용 프로토콜이라고 하는데, 프로토콜 정의 시 class 키워드를 사용하여 클래스 전용 프로토콜임을 컴파일러에게 알려준다.
protocol SomeClassOnlyProtocol: class {
// 클래스에서 구현할 내용 작성
}클래스 전용 프로토콜에서는 메소드를 정의할 때 mutating 키워드를 붙일 수 없다. Why?
원래 mutating 키워드는 구조체나 열거형 등 클래스가 아닌 객체가 메소드 내에서 프로퍼티를 수정할 수 있게 하기 위한 목적으로 사용하기 때문에 구조체나 열거형이 구현할 수 없는 클래스 전용 프로토콜에서는 사용할 필요가 없다.
이와는 달리 static 키워드는 클래스에서도 이용하기 때문에 클래스 전용 프로토콜에서도 제약 없이 사용 가능하다.
만약 프로토콜이 다른 프로토콜을 상속받는다면, 상속된 프로토콜 이름들을 나열하기 전에 맨 먼저 클래스 전용임을 표시해야 한다.
class 키워드와 상속 프로토콜 이름을 작성할 때는 class 키워드를 맨 앞에 작성해야 한다.protocol SomeClassOnlyProtocol: class, Wheel, Machine {
// 클래스에서 구현할 내용 작성
}optional
프로토콜에서 사용되는 optional 키워드에 대해 설명한다.
프로토콜을 구현할 때는 기본적으로 프로토콜의 명세에 포함된 모든 프로퍼티와 메소드, 그리고 초기화 구문을 구현해야 한다.
그렇지 않으면 필요한 항목의 구현이 누락되었다는 오류가 발생한다.
하지만 구현하는 객체에 따라 특별히 필요하지 않은 프로퍼티나 메소드, 초기화 구문이 있을 수 있다. 이런 상황을 방지하기 위한 방법이 선택적 요청(Optional Requirement)이라고 불리는 문법이다.
이 문법은 프로토콜에서 선언된 프로퍼티나 메소드, 초기화 구문 등 프로토콜을 구현할 때 작성해야 하는 요소들을 선택 사항으로 바꿔준다.
프로토콜에서 optional 키워드를 사용하려면 약간의 제약이 있다. 프로토콜 앞에 @objc를 표시해야 한다.
(@objc는 Foundation 프레임워크에 정의된 어노테이션의 일종으로, 이 어노테이션이 붙은 코드나 객체를 Objective-C 코드에서도 참조할 수 있도록 노출됨을 의미한다.)
또한 @objc 어노테이션이 붙은 프로토콜은 구조체나 열거형 등에서 구현할 수 없고 오로지 클래스만이 해당 프로토콜을 구현할 수 있다.
optional 키워드가 붙은 선택적 요청 프로토콜은 클래스만 구현할 수 있다는 뜻이다. 즉, optional 키워드는 클래스 전용 프로토콜임을 뜻한다.
@objc protocol MsgDelegate { // 새로운 메시지가 도착했을 때 새로운 메시지의 개수를 델리게이트로 할당된 객체에 알려주는 역할 @objc optional func onReceive(new: Int) } class MsgCenter { var delegate: MsgDelegate? var newMsg: Int = 0 func msgCheck() { if newMsg > 0 { // 새로운 메시지가 도착했다면 self.delegate?.onReceive?(new: self.newMsg) self.newMsg = 0 } } } class MsgWatch: MsgDelegate { var msgCenter: MsgCenter? init(msgCenter: MsgCenter) { self.msgCenter = msgCenter } func onReceive(new: Int) { print("\(new)건의 메시지가 도착했습니다.") } }
MsgDelegate의 onReceive(new:) 메소드는 optional 키워드가 추가되어 있으므로 반드시 구현하지 않아도 된다.
실제로 메시지를 받고 처리하는 MsgCenter 클래스는 델리게이션 구현과 유사한 구조이다.
msgCheck() 메소드가 호출되면 새로운 메시지가 있는지 없는지를 검사해서 있을 때는 델리게이트로 할당된 객체의 onReceive(new:) 메소드를 호출한 후, 새로운 메시지의 개수를 0으로 설정한다.
이때 optional 키워드가 붙은 메소드를 호출할 때는 옵셔널 체인처럼 사용하면 된다.
(하지만 이때는 메소드의 결과값이 옵셔널이 아니라 메소드 자체가 옵셔널이므로 메소드와 괄호 사이에 ? 연산자를 작성해야 한다.)
MsgWatch 클래스에서는 onReceive(new:) 메소드를 구현하여 새로운 메시지의 개수를 출력한다.
onReceive(new:) 메소드는 필수로 구현하지 않아도 되며, 구현하지 않았을 경우 새로운 메시지 도착에 대한 알림을 못받는 것이다.
실제로 코코아 터치 프레임워크에서는 프로토콜마다 정의해야할 메소드가 상당히 많다.
그래서 해당 프로토콜에서 반드시 필요한 메소드만을 제외하고 나머지는 optional 키워드로 선언뙤어 선택적으로 구현할 수 있도록 제공하고 있다.
'iOS > Swift' 카테고리의 다른 글
[Swift] ARC 알아보기 (Auto Reference Counting) (0) 2020.09.04 Closure (2) (0) 2020.04.17 Closure (0) 2020.04.11 프로토콜 지향언어, Swift (2) (1) 2020.03.14 프로토콜지향 언어, Swift (1) (0) 2020.03.07