개발하는 뚝딱이

[swift] 프로토콜 본문

swift

[swift] 프로토콜

개발자뚝딱이 2020. 9. 21. 19:19

 

책 <스위프트 프로그래밍 3판 ;야곰 지음> 을 정리한 글입니다.

 


 

 

1. 프로토콜 (Protocol)

- 프로토콜은 특정 역할을 하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진

- 구조체, 클래스, 열거형은 프로토콜을 채택해서 특정 기능을 실행하기 위한 프로토콜의 요구사항을 실제로 구현할 수 있음

- 프로토콜의 요구사항을 모두 따르는 타입은 '해당 프로토콜을 준수한다 (conform)'이라고 표현

- 프로토콜은 정의를 하고 제시를 한 뿐, 스스로 기능을 구현하지 않음

 

 

2. 채택

protocol AProtocol {
    // 프로토콜 정의
}

protocol AnotherProtocol {
    // 프로토콜 정의
}

struct SomeStruct: AProtocol, AnotherProtocol {
    // 구조체 정의
}

class SomeClass: AProtocol, AnotherProtocol {
    // 클래스 정의
}

enum SomeEnum: AProtocol, AnotherProtocol {
    // 열거형 정의
}

 

 

3. 프로토콜 요구사항

 

3.1 프로퍼티 요구

- 프로토콜 요구사항은 항상 var 키워드를 사용한 변수 프로퍼티로 정의

- 읽고 쓰기가 모두 가능한 프로퍼티는 { get set }

- 읽기 전용 프로퍼티는 프로퍼티의 정의 뒤에 { get } 이라고 명시해줌

 

- 타입 프로퍼티를 요구하려면 static 키워드 사용

protocol SomeProtocol {
    var settableProperty: String { get set }
    var notNeedToBeSettable: String { get }
}

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
    static var anotherTypeProperty: Int { get }
}

 

- Sendable 프로토콜에선 읽기 전용 프로토콜로 from을 정의했으나

  채택하는 클래스에선 읽고 쓰기가 가능한 프로퍼티로 구현해도 문제 없음

// Sendable 프로토콜과 Sendable 프로토콜을 준수하는 Message와 Mail 클래스

protocol Sendable {
    var from: String { get }
    var to: String { get }
}

class Message: Sendable {
    var sender: String
    var from: String { // 읽기 전용
        return self.sender
    }
    
    var to: String
    
    init(sender: String, receiver: String) {
        self.sender = sender
        self.to = receiver
    }
}

class Mail: Sendable {
    var from: String // 읽고 쓰기 가능
    var to: String
    
    init(sender: String, receiver: String) {
        self.from = sender
        self.to = receiver
    }
}

 

3.2 메서드 요구

- 프로토콜 정의에서 특정 인스턴스 메서드나 타입 메서드를 요구할 수 있음

- 다만 메서드의 실제 구현부인 중괄호 {} 부분은 제외하고

  메서드의 이름, 매개변수, 반환 타입 등만 작성해 가변 매개변수도 허용

 

- 프로토콜의 메서드 요구에서는 매개변수 기본값을 지정할 수 없음

- 타입 메서드를 요구할 때는 타입 프로퍼티의 요구와 마찬가지로 static 키워드로 명시

- static 키워드를 사용하여 요구한 타입 메서드를 클래스에서

  실제 구현할 때는 static 키워드나 class 키워드 어느 쪽을 사용해도 무방

 

// 무언가를 발신할 수 있는 기능
protocol Sendable {
    var from: Sendable { get }
    var to: Receiveable? { get }
    
    func send(data: Any)
    
    static func isSendableInstance(_ instance: Any) -> Bool
}

// 무언가를 수신받을 수 있는 기능
protocol Receiveable {
    func received(data: Any, from: Sendable)
}


class Message: Sendable, Receiveable {
    // 발신은 Sendable 프로토콜을 준수하는 타입의 인스턴스여야 함
    var from: Sendable {
        return self
    }
    
    // 상대방은 수신 가능한 객체, 즉 Receivable 프로토콜을 준수하는 타입의 인스턴스여야 함
    var to: Receiveable?
    
    // 메시지를 발신
    func send(data: Any) {
        guard let receiver: Receiveable = self.to else {
            print("Message has no receiver")
            return
        }
        
        // 수신 가능한 인스턴스의 received 메서드 호출
        receiver.received(data: data, from: self.from)
    }
    
    // 메시지를 수신
    func received(data: Any, from: Sendable) {
        print("Message received \(data) from \(from)")
    }
    
    // class 메서드이므로 상속이 가능
    class func isSendableInstance(_ instance: Any) -> Bool {
        if let sendableInstance: Sendable = instance as? Sendable {
            return sendableInstance.to != nil
        }
        
        return false
    }
}

// 수신, 발신이 가능한 Mail 클래스
class Mail: Sendable, Receiveable {
    var from: Sendable {
        return self
    }
    
    var to: Receiveable?
    
    func send(data: Any) {
        guard let receiver: Receiveable = self.to else {
            print("Mail has no received")
            return
        }
        
        receiver.received(data: data, from: self.from)
    }
    
    func received(data: Any, from: Sendable) {
        print("Mail received \(data) from \(from)")
    }
    
    // static 메서드이므로 상속이 불가능
    static func isSendableInstance(_ instance: Any) -> Bool {
        if let sendableInstance: Sendable = instance as? Sendable {
            return sendableInstance.to != nil
        }
        
        return false
    }
}

// 두 Message 인스턴스 생성
let myPhoneMessage: Message = Message()
let yourPhoneMessage: Message = Message()

myPhoneMessage.send(data: "Hello") // Message has no receiver

myPhoneMessage.to = yourPhoneMessage
myPhoneMessage.send(data: "Hello") // Message received Hello from Message

// 두 Mail 인스턴스 생성
let myMail: Mail = Mail()
let yourMail: Mail = Mail()

myMail.send(data: "Hi") // Mail has no received
myMail.to = yourMail
myMail.send(data: "Hi") // Mail received Hi from Mail

// Mail과 Message 모두 Sendable과 Receiveable 프로토콜을 준수하므로 서로 주고받을 수 있다
myMail.to = myPhoneMessage
myMail.send(data: "Bye") // Message received Bye from Mail

// String은 Sendable 프로토콜을 준수하지 않음
print(Message.isSendableInstance("Hello")) // false
print(Message.isSendableInstance(myPhoneMessage)) // true

// yourPhoneMessage는 to 프로퍼티가 설정되지 않아서 보낼 수 없음
print(Message.isSendableInstance(yourPhoneMessage)) // false
print(Mail.isSendableInstance(myPhoneMessage)) // true
print(Mail.isSendableInstance(myMail)) // true



3.3 가변 메서드 요구

- 값 타입 (구조체와 열거형)의 인스턴스 메서드에서 자신 내부의 값을 변경하고자 할 때는 func 앞에 mutating을 적어 메서드에서 인스턴스 내부의 값을 변경한다는 것을 표현

- class 구현에서는 mutating 키워드를 안써도 됨

 

- Resettable 프로토콜에서 가변 메서드를 요구하지 않으면, 값 타입의 인스턴스 내부 값을 변경하는 mutating 메서드는 구현이 불가능

protocol Resettable {
    mutating func reset() // 가변메서드 요구
}

class Person: Resettable {
    var name: String?
    var age: Int?
    
    func reset() {
        self.name = nil
        self.age = nil
    }
}

struct Point: Resettable {
    var x: Int = 0
    var y: Int = 0
    
    mutating func reset() {
        self.x = 0
        self.y = 0
    }
}

enum Direction: Resettable {
    case east, west, south, north, unknown
    
    mutating func reset() {
        self = Direction.unknown
    }
}

 

3.4 이니셜라이저 요구

- 프로퍼티, 메서드 등과 같이 이니셜라이저를 정의할 뿐 구현하지 않음

- 클래스 타입에서 프로토콜의 이니셜라이저 요구에 부합하는 이니셜라이저를 구현할 때는 지정 이니셜라이저인지

  편의 이니셜라이저인지 중요하지 않음

- 대신 이니셜라이저 요구에 부합하는 이니셜라이저를 구현할 때는 required 식별자를 붙인 요구 이니셜라이저로 구현해야 함

protocol Named {
    var name: String { get }
    
    init(name: String)
}

struct Pet: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Person: Named {
    var name: String
    
    required init(name: String) {
        self.name = name
    }
}

 

- 만약 클래스 자체가 상속받을 수 없는 final 클래스라면 required 식별자를 붙여줄 필요가 없음

- 상속할 수 없는 클래스의 요청 이니셜라이저 구현은 무의미하기 때문

final class Person: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

 

- 만약 특정 클래스에 프로토콜이 요구하는 이니셜라이저가 이미 구현되어 있는 상황에서 그 클래스를 상속받은 클래스가

   있다면, requirred와 override 식별자를 모두 명시하여 프로토콜에서 요구하는 이니셜라이저를 구현해줘야 함

- requirred와 override 위치는 상관없음

protocol Named {
    var name: String { get }
    
    init(name: String)
}


class School {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class MiddleSchool: School, Named {
    required override init(name: String) {
        super.init(name: name)
    }
}

 

- 실패 가능한 이니셜라이저 요구를 포함하는 Named 프로토콜과 Named 프로토콜을 준수하는 다양한 타입들

- 실패 가능한 이니셜라이저를 요구하는 프로토콜을 준수하는 타입은, 해당 이니셜라이저를 구현할 때

   실패 가능한 이니셜라이저와 일반적인 이니셜라이저 상관없이 구현 가능

protocol Named {
    var name: String { get }
    
    init?(name: String)
}

struct Animal: Named {
    var name: String
    
    init!(name: String) {
        self.name = name
    }
}

struct Pet: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Person: Named {
    var name: String
    
    required init(name: String) {
        self.name = name
    }
}

class School: Named {
    var name: String
    
    required init(name: String) {
        self.name = name
    }
}

 

4. 프로토콜의 상속과 클래스 전용 프로토콜

- 프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항보다 더 많은 요구사항 추가 가능

- 프로토콜 상속 문법은 클래스의 상속 문법과 유사

protocol Readable {
    func read()
}

protocol Writeable {
    func write()
}

protocol ReadSpeakable: Readable {
    func speak()
}

protocol ReadWriteSpeakable: Readable, Writeable {
    func speak()
}

class SomeClass: ReadWriteSpeakable {
    func read() {
        print("Read")
    }
    
    func write() {
        print("Write")
    }
    
    func speak() {
        print("Speak")
    }
}

 

- 클래스 전용 프로토콜 정의

- 프로토콜의 상속 리스트에 class 키워드를 추가해 프로토콜이 클래스 타입에만 채택될 수 있도록 제한할 수 있음

- 클래스 전용 프로토콜로 제한하기 위해 프로토콜의 상속 리스트의 맨 처음에 class 키워드가 위치해야 함

protocol ClassOnlyProtocol: class, Readable, Writeable {
    // 추가 요구사항
}

class SomeClass: ClassOnlyProtocol {
    func read() { }
    func write() { }
}

// 오류! ClassOnlyProtocol 프로토콜은 클래스 타입에만 채택 가능
struct SomeStruct: ClassOnlyProtocol {
    func read() { }
    func write() { }
}

 

5. 프로토콜 조합과 프로토콜 준수 확인

- 하나의 매개변수가 여러 프로토콜을 모두 준수하는 타입이어야 한다면 하나의 매개변수에 여러 프로토콜을

  한 번에 조합하여 요구 할 수 있음

- 프로토콜을 조합하여 요구할 때는 SomeProtocol & AnotherProtocol과 같이 표현

- 또, 하나의 매개변수가 프로토콜을 둘 이상 요구할 수도 있음. 이 때도 &를 여러 프로토콜 이름 사이에 써줌

 

- 특정 클래스의 인스턴스 역할을 할 수 있는지 확인 가능

- 구조체나 열거형 타입은 조합할 수 없음

- 조합 중 클래스 타입은 한 타입만 조합 가능

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

struct Person: Named, Aged {
    var name: String
    var age: Int
}

class Car: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class Truck: Car, Aged {
    var age: Int
    
    init(name: String, age: Int) {
        self.age = age
        super.init(name: name)
    }
}

func celebrateBirthday(to celebrator: Named & Aged) {
    print("Happy birthday \(celebrator.name)!! Now you are \(celebrator.age)")
}

let yagom: Person = Person(name: "yagom", age: 99)
celebrateBirthday(to: yagom) // Happy birthday yagom!! Now you are 99

let myCar: Car = Car(name: "Boong Boong")
//celebrateBirthday(to: myCar) // Aged를 충족하지 않음

//var someVariable: Car & Truck & Aged // 클래스 & 프로토콜 좋바에서 클래스는 한 타입만 가능

var someVariable: Car & Aged
someVariable = Truck(name: "Truck", age: 5)

someVariable = Truck(name: "Tuesday", age: 123)
//someVariable = myCar // Aged를 충족하지 않음

 

- 프로토콜도 하나의 타입이므로, 데이터의 타입 캐스팅을 똑같이 사용 가능

 

- is 연산자를 통해 해당 인스턴스가 특정 프로토콜을 준수하는지 확인할 수 있음

- as? 다운캐스팅 연산자를 통해 다른 프로토콜로 다운캐스팅 시도 가능

- as! 다운캐스팅 연산자를 통해 다른 프로토콜로 강제 다운캐스팅 가능

print(yagom is Named) // true
print(yagom is Aged) // true

print(myCar is Named) // true
print(myCar is Aged) // false

if let castedInstance: Named = yagom as? Named {
    print("\(castedInstance) is Named")
    // Person(name: "yagom", age: 99) is Named
}

if let castedInstance: Aged = yagom as? Aged {
    print("\(castedInstance) is Aged")
    // Person(name: "yagom", age: 99) is Aged
}

if let castedInstance: Named = myCar as? Named {
    print("\(castedInstance) is Named")
    // Car is Named
}

if let castedInstance: Aged = myCar as? Aged {
    print("\(castedInstance) is Aged")
    // 출력 없음, 캐스팅 실패
}

 

 

6. 프로토콜의 선택적 요구

- 프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정 가능

- 선택적 요구사항을 정의하고 싶은 프로토콜은 objc 속성이 부여된 프로토콜이여야 함

- objc 속성이 부여되는 프로토콜은 Objective-C클래스를 상속받은 클래스에서만 채택 가능

- 열거형이나 구조체 등에서는 objc 속성이 부여된 프로토콜은 아예 채택 불가능

 

- 선택적 요구사항은 optional 식별자를 요구사항의 정의 앞에 붙여주면 됨

- 만약 메서드나 프로퍼티를 선택적 요구사항으로 요구하게 되면, 요구사항의 타입은 자동적으로 옵셔널이 됨

- 메서드의 매개변수나 반환타입이 옵셔널이 된 것이 아니라 메서드 자체의 타입이 옵셔널이 된 것

 

- 선택적 요구사항은 그 프로토콜을 준수하는 타입에 구현되어 있지 않을 수 있기 때문에 옵셔널 체이닝을 통해 호출 가능

import Foundation // @objc 사용을 위한 Foundation import

@objc protocol Moveable {
    func walk()
    @objc optional func fly()
}

// 걷기만 할 수 있는 호랑이
class Tiger: NSObject, Moveable {
    func walk() {
        print("Tiger walks")
    }
}

// 걷고 날 수 있는 새
class Bird: NSObject, Moveable {
    func walk() {
        print("Bird walks")
    }
    
    func fly() {
        print("Bird flys")
    }
}

let tiger: Tiger = Tiger()
let bird: Bird = Bird()

tiger.walk() // Tiger walks
bird.walk() // Bird walks

bird.fly() // Bird flys

var moveableInstance: Moveable = tiger
moveableInstance.fly?() // nil

moveableInstance = bird
moveableInstance.fly?() // Bird flys

 

 

7. 프로토콜 변수와 상수

- 프로토콜 이름을 타입으로 갖는 변수 또는 상수에는 그 프로토콜을 준수하는 타입의 어떤 인스턴스라도 할당 가능

protocol Named {
    var name: String { get }
}

struct Animal: Named {
    var name: String
    
    init!(name: String) {
        self.name = name
    }
}

struct Pet: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}

class School: Named {
    var name: String
    
    required init(name: String) {
        self.name = name
    }
}

class Person: Named {
    var name: String
    
    required init(name: String) {
        self.name = name
    }
}



var someNamed: Named = Animal(name: "Animal")
someNamed = Pet(name: "Pet")
someNamed = Person(name: "Person")
someNamed = School(name: "School")

 

8. 위임을 위한 프로토콜

- 위임 Delegate 은 클래스나 구조체가 자신의 책임이나 임무를 다른 타입의 인스턴스에게 위임하는 디자인패턴

- 책무를 위임하기 위해 정의한 프로토콜을 준수하는 타입은 자신에게 위임될 일정 책무를 할 수 있다는 것을 보장

- 사용자의 특정 행동에 반응하기 위해 사용되기도 하며, 비동기 처리에도 많이 사용

 

- 위임 패턴 Delegate Pattern은 애플의 프레임워크에서 사용하는 주요한 패턴 중 하나

- 위임 패턴을 위해 다양한 프로토콜이 'OOOODelegate'라는 식의 이름으로 정의되어 있음

 

 

 

'swift' 카테고리의 다른 글

[swift] 스위프트를 더 스위프트스럽게 사용하기  (0) 2021.03.11
[swift] 프로토콜 초기구현  (0) 2020.09.28
[swift] 상속  (0) 2020.09.05
[swift] 서브스크립트  (0) 2020.06.25
[swift] 모나드  (0) 2020.06.23