프로토콜

피터·2022년 9월 11일
0
post-thumbnail

프로토콜이란

프로토콜은 특정 역할을 하기 위한 메서드, 프로퍼티, 기타 요구사항 등의 청사진을 정의합니다. 구조체, 클래스, 열거형은 프로토콜을 채택해서 프로토콜 요구 사항을 실제로 구현할 수 있으며 어떤 프로토콜의 요구사항을 모두 따르는 타입은 ‘해당 프로토콜을 준수한다.’고 표현합니다.

프로토콜은 정의를 하고 제시를 할 뿐 스스로 기능을 구현하지는 않습니다.

💡 프로토콜지향프로그래밍, 프로토콜은 왜 사용하는 것일까?

프로토콜 채택

  • 프로토콜의 구현
    protocol 프로토콜 이름 {
    	프로토콜 정의
    }
  • 프로토콜 채택
    struct AStruct: AProtocol, BProtocol {
    	// 구조체 정의
    }
    
    class AClass: AProtocol, BProtocol {
    	// 클래스 정의
    }
    
    enum SomeEnum: AProtocol, BProtocol {
    	// 열거형 정의
    }
    프로토콜은 다중 채택이 가능합니다.

프로토콜 요구사항

프로토콜은 타입이 특정 기능을 실행하기 위해 필요한 기능을 요구합니다. 프로토콜이 자신을 채택한 타입에 요구하는 사항은 프로퍼티나 메서드 같은 기능들입니다.

프로퍼티 요구

프로토콜은 자신을 채택한 타입이 어떤 프로퍼티를 구현해야 하는지 요구할 수 있습니다. 그렇지만 프로토콜은 그 프로퍼티의 종류(연산 프로퍼티인지, 저장 프로퍼티인지 등)는 따로 신경쓰지 않습니다.

프로토콜이 요구하는 프로퍼티의 이름과 타입만 맞으면 됩니다.

다만 프로퍼티를 읽기 전용으로 할지 혹은 읽고 쓰기가 모두 가능하게 할지는 프로토콜이 정해야 합니다.

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

struct Message: MessageType {
    var sender: String
    
    var from: String {
        return self.sender
    }
    
    var to: String
    
    init(sender: String, receiver: String) {
        self.sender = sender
        self.to = receiver
    }
    
}

struct Email: MessageType {
    var from: String
    var to: String
    
    init(sender: String, receiver: String) {
        self.from = sender
        self.to = receiver
    }
}

메서드 요구

프로토콜은 특정 인스턴스 메서드나 타입 메서드를 요구할 수도 있습니다.

하지만 요구만 할 뿐, 메서드의 실제 구현부분은 제외하고 메서드의 이름, 매개변수, 반환 타입 등만 작성합니다.

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

타입 메서드를 요구할 때는 타입 프로퍼티 요구와 마찬가지로 앞에 static 키워드를 명시합니다.

protocol Sendable {
    var from: Sendable { get }
    var to: Receivable? { get }
    
    func send(data: Any)
    
    static func isSendableInstance(_ instance: Any) -> Bool
}

protocol Receivable {
    func received(data: Any, from: Sendable)
}

class Message: Sendable, Receivable {
    var from: Sendable {
        return self
    }
    
    var to: Receivable?
    
    func send(data: Any) {
        guard let receiver = self.to else {
            print("메세지를 받을 사람이 없습니다.")
            return
        }
        
        receiver.received(data: data, from: self.from)
    }
    
    static func isSendableInstance(_ instance: Any) -> Bool {
        if let sendableInstance = instance as? Sendable {
            return sendableInstance.to != nil
        }
        return false
    }
    
    func received(data: Any, from: Sendable) {
        print("\(data)메세지가 도착했습니다. from: \(from)")
    }
}

class Mail: Sendable, Receivable {
    var from: Sendable {
        return self
    }
    
    var to: Receivable?
    
    func send(data: Any) {
        guard let receiver: Receivable = self.to else {
            print("메일을 받을 사람이 없습니다.")
            return
        }
        
        receiver.received(data: data, from: self.from)
    }
    
    static func isSendableInstance(_ instance: Any) -> Bool {
        if let sendableInstance: Sendable = instance as? Sendable {
            return sendableInstance.to != nil
        }
        return false
    }
    
    func received(data: Any, from: Sendable) {
        print("\(data) 메일이 도착했습니다. from: \(from)")
    }
}

let myPhoneMessage = Message()
let yourPhoneMessage = Message()

myPhoneMessage.send(data: "안녕하세요~!")
// "메세지를 받을 사람이 없습니다."
 
myPhoneMessage.to = yourPhoneMessage
myPhoneMessage.send(data: "다시 한번, 안녕하세요~!")
// "다시 한번, 안녕하세요~!메세지가 도착했습니다.from: Message" 

let myMail = Mail()
let yourMail = Mail()

myMail.send(data: "메일은 연결이 되나?")
// "메일을 받을 사람이 없습니다."

// 수신자 표시
myMail.to = yourPhoneMessage

myMail.send(data: "안녕하시오.")
// "안녕하시오.메세지가 도착했습니다. from: Mail"

myMail.to = myPhoneMessage
myMail.send(data: "이 사진 좀 보관해놔")
// "이 사진 좀 보관해놔메세지가 도착했습니다. from Mail"

Message.isSendableInstance("Hello") // false 
Message.isSendableInstance(myPhoneMessage) // true

Message.isSendableInstance(yourPhoneMessage) // false
Mail.isSendableInstance(myPhoneMessage) // true
Mail.isSendableInstance(myMail) // true

💡 타입으로서의 프로토콜
프로토콜은 요구만 하고 스스로 기능을 구현하지는 않지만 코드에서 완전히 하나의 타입으로 사용됩니다.

  • 함수, 메서드, 이니셜라이저에서 매개변수 타입이나 반환 타입으로 사용
  • 프로퍼티, 변수, 상수 등의 타입으로 사용
  • 배열, 딕셔너리 등 컨테이너 요소의 타입

가변 메서드 요구

프로토콜이 인스턴스 내부의 값을 변경해야 하는 메서드를 요구하려면 프로토콜의 메서드 정의 앞에 mutating 키워드를 명시해야 합니다.

참조 타입인 클래스의 메서드 앞에는 mutating 키워드를 명시하지 않아도 인스턴스 내부 값을 바꾸는 데 문제가 없지만, 값 타입인 구조체와 열거형의 메서드 앞에는 mutating 키워드를 붙인 가변 메서드 요구가 필요합니다.

만약 가변 메서드 요구를 하지 않는다면, 값 타입의 인스턴스 내부 값을 변경하는 mutating 메서드 구현이 불가능 합니다.

protocol Resettable {
    mutating func reset()
}

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

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

이니셜라이저 요구

프로토콜은 특정한 이니셜라이저를 요구할 수도 있습니다.

단, 이니셜라이저의 매개변수를 지정할 뿐 이니셜라이저 구현은 하지 않습니다.

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

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

구조체는 상속할 수 없기 때문에 이니셜라이저 요구에 크게 신경쓸 필요가 없습니다.

클래스에서는 다릅니다.

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

프로토콜의 이니셜라이저 요구에 부합하는 이니셜라이저를 구현할 때는 지정인지 편의 이니셜라이저인지 중요하지 않지만 required 식별자를 붙인 요구 이니셜라이저로 구현해야 합니다.

예외적으로, 클래스 자체가 상속받을 수 없는 final 클래스라면 required 식별자를 붙여주지 않아도 됩니다.

final class Pet: Named {
    var name: String
    
    init(name: String) {
        self.name = name
    }
}
// 오류 생기지 않음 

다음과 같은 경우는 어떨까요?

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

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

School에서는 Named 프로토콜을 채택하지 않았지만 Named 프로토콜이 요구하는 이니셜라이저가 이미 있는 상태입니다.
그런데 MiddleSchool 클래스는 School 클래스를 상속받았으며 Named 프로토콜을 채택한 상황입니다.
그래서 School 클래스의 init(name:) 이니셜라이저를 재정의해야 하며 동시에 Named 프로토콜의 이니셜라이저를 충족시켜줘야하기 때문에 override와 required 식별자를 모두 표기해야 합니다.

프로토콜은 실패 가능한 이니셜라이저를 요구할 수 있습니다.

실패 가능한 이니셜라이저를 요구하는 프로토콜을 준수하는 타입은 해당 이니셜라이저를 구현할 때 실패 가능한 이니셜라이저로 구현해도 되고, 일반적인 이니셜라이저로 구현해도 무방합니다.

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

프로토콜은 하나 이상의 프로토콜을 상속받아 기존 프로토콜의 요구사항보다 더 많은 요구사항을 추가할 수 있습니다.

protocol Readable {
    func read()
}

protocol Writable {
    func write()
}

protocol ReadSpeakable: Readable {
    func speak()
}

protocol ReadWriteSpeakable: Readable, Writable {
    func speck()
}

class SomeClass: ReadWriteSpeakable {
    func speck() {
        print(#function)
    }
    
    func read() {
        print(#function)
    }
    
    func write() {
        print(#function)
    }
}

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

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

하나의 매개변수가 여러 프로토콜을 모두 준수하는 타입이어야 한다면 하나의 매개변수에 여러 프로토콜을 한 번에 조합하여 요구할 수 있습니다.

프로토콜을 조합할 경우에는 다음과 같이 표현합니다.

SomeProtocol & AnotherProtocol

하나의 매개변수가 프로토콜을 둘 이상 요구할 수 도 있습니다.

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

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

func 생일축하합니다(to celebrator: Aged & Named) {
    print("\(celebrator.name)\(celebrator.age)번째 생일을 축하합니다!!")
}

let peter = Person(name: "피터", age: 31)

생일축하합니다(to: peter)
// 피터씨 31번째 생일을 축하합니다!!

다음과 같이 매개변수에서 조합되는 프로토콜을 모두 채택하지 않은 인스턴스를 넣는다면 오류가 발생합니다.

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)
    }
}

let spark = Car(name: "피카츄")
생일축하합니다(to: spark) // Aged 프로토콜을 채택하지 않았으므로 오류 발생

구조체나 열거형 타입은 조합할 수 없습니다.

protocol HouseOwned {
    var address: String { get }
}

let valueTypeCombine: Person & HouseOwned
// 오류 발생!!

조합 중 클래스 타입은 한 타입만 조합할 수 있습니다.

let referenceTypeCombine: Car & Truck & HouseOwned
// 오류 발생 !! 클래스의 조합은 한 타입만 가능 

타입 캐스팅에 사용하던 is와 as 연산자를 통해 대상이 프로토콜을 준수하는지 확인할 수도 있고, 특정 프로토콜로 캐스팅할 수 있습니다.

프로토콜의 선택적 요구

프로토콜의 요구사항 중 일부를 선택적 요구사항으로 지정할 수 있습니다. 다만 먼저 고려해야할 사항이 있습니다.

선택적 요구사항을 정의하고 싶은 프로토콜은 objc 속성(Objective-C) 코드에서 사용할 수 있도록 만드는 역할을 합니다.

여기서 추가적으로 objc 속성이 부여되는 프로토콜은 Objective-C 클래스를 상속받은 클래스에서만 채택할 수 있다는 것입니다. 즉, 열거형이나 구조체 등에서는 objc 속성이 부여된 프로토콜은 아예 채택할 수 없습니다.

선택적 요구를 하면 프로토콜을 준수하는 타입에 해당 요구사항을 필수로 구현할 필요가 없습니다. 선택적 요구사항은 optional 식별자를 요구사항의 정의 앞에 붙여주면 됩니다.

또한 선택적 요구사항으로 메서드나 프로퍼티를 요구하게 되면 그 요구사항의 타입은 자동적으로 옵셔널이 됩니다.

예를 들어 (Int) → String 타입의 메서드는 ((Int) → String)? 타입으로 메서드 자체가 옵셔널이 됩니다.

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

class Tiger: NSObject, Moveable {
    func walk() {
        print("호랑이는 걸을 수 있습니다.")
    }
}

class Bird: NSObject, Moveable {
    func walk() {
        print("새는 걸을 수 있습니다.")
    }
    
    func fly() {
        print("새는 날 수도 있죠.")
    }
}

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

tiger.walk()
// 호랑이는 걸을 수 있습니다.
bird.walk()
// 새는 걸을 수 있습니다.

var moveableInstance: Moveable = tiger

moveableInstance.fly?()
// 반응 없음 

moveableInstance = bird

moveableInstance.fly?()
// 새는 날 수도 있죠.

프로토콜 변수와 상수

프로토콜 이름을 타입으로 갖는 변수 도는 상수에는 그 프로토콜을 준수하는 타입의 어떤 인스턴스라도 할당할 수 있습니다.

var namedInstance: Named = Person(name: "이름", age: 1)
namedInstance = Car(name: "자동차")
namedInstance = Truck(name: "트럭", age: 2)

위임을 위한 프로토콜

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

델리게이트 패턴은 애플의 프레임워크에서 사용하는 주요한 패턴 중 하나입니다.

자료 출처: 야곰 스위프트 프로그래밍 3판

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
profile
iOS 개발자입니다.

0개의 댓글