본문 바로가기
Xcode/Swift - PlayGround

PlayGround) Protocol에 대해서 좀 더 깊게 알아가기

by 후르륵짭짭 2021. 2. 14.
728x90
반응형

안녕하세요 후르륵 짭짭입니다.

이번에는 프로토콜에 대해서 좀 더 알아보려고 합니다!

이전에 한번 글을 작성 한 적이 있는데,

프로토콜은 규칙이라고 설명 한 적이 있습니다.

하지만 한번도 깊게 다뤄 본적이 없기 때문에 이번 기회에 깊게 다뤄볼 생각 입니다.

그러나 기본적인 내용 보다는 응용면을 좀 더 다뤄 볼까 합니다.

 

** 타입으로서 프로토콜 **

protocol AttackerProtocol{
    
    var attacker : AttackerProtocol? {get set}
    
    func attack()
    
    func attackWay()
    
}

protocol DefenceProtocol{
    
    var name : String {get set}
    
    var defencer : DefenceProtocol? {get set}
    
    func defence(instance : DefenceProtocol, attacker : AttackerProtocol & DefenceProtocol)
    
    func defecneWay(attacker : AttackerProtocol & DefenceProtocol)
    
}

class Player_A : AttackerProtocol , DefenceProtocol {
    
    //AttackerProtocol
    var attacker: AttackerProtocol?
    
    func attack() {
        guard let att = self.attacker else {
            print("\(self.name) attacker is nil")
            return}
        
        att.attackWay()
    }
    
    func attackWay() {
        print("\(self.name) use Player_A Attack")
    }
    
    //DefenceProtocol
    
    var name: String
    
    var defencer: DefenceProtocol?
    
    func defence(instance : DefenceProtocol, attacker : AttackerProtocol & DefenceProtocol) {
        guard let defence = self.defencer else {
            instance.defecneWay(attacker: attacker)
            return
        }
        
        defence.defecneWay(attacker: attacker)
    }
    
    func defecneWay(attacker : AttackerProtocol & DefenceProtocol) {
        print("\(self.name) defence \(attacker.name) with Player_A Style")
    }
    
    init(name : String){
        self.name = name
        self.attacker = self
        self.defencer = self
    }

    
}

class Player_B : AttackerProtocol , DefenceProtocol {
    
    //AttackerProtocol
    var attacker: AttackerProtocol?
    
    func attack() {
        guard let att = self.attacker else {
            print("\(self.name) attacker is nil")
            return}
        
        att.attackWay()
    }
    
    func attackWay() {
        print("\(self.name) use Player_B Attack")
    }
    
    //DefenceProtocol
    
    var name: String
    
    var defencer: DefenceProtocol?
    
    func defence(instance : DefenceProtocol, attacker : AttackerProtocol & DefenceProtocol) {
        guard let defence = self.defencer else {
            instance.defecneWay(attacker: attacker)
            return
        }
        
        defence.defecneWay(attacker: attacker)
    }
    
    func defecneWay(attacker : AttackerProtocol & DefenceProtocol) {
        print("\(self.name) defence \(attacker.name) with Player_B Style")
    }
    
    init(name : String){
        self.name = name
        self.attacker = self
        self.defencer = self
    }

}

이 코드를 보면 AttackerProtocol과 DefenceProtocol 두개가 있습니다.

그리고 AttackerProtocol과 DefenceProtocol 에는 프로터피와 메소드 들이 정의 되어 있습니다.

여기서 주의 깊게 봐야할 점은 

//AttackerProtocol

var attacker : AttackerProtocol? {get set}

//DefenceProtocol

var defencer : DefenceProtocol? {get set}

이 두개 입니다.

이 두 프로퍼티가 모두 Protocol을 타입으로 가지고 있습니다.

이게 무슨 말이냐 하면,,,, Protocol을 준수하는 인스턴스를 가질 수 있다는 것을 의미합니다.

class Player_A : AttackerProtocol , DefenceProtocol {
    
    //AttackerProtocol
    var attacker: AttackerProtocol?
    
    ,,, 생략 ,,,
    
    init(name : String){
        self.name = name
        self.attacker = self
        self.defencer = self
    }

    
}

이렇게 Player_A 클래스는 AttackerProtocol을 준수하면서도 DefenceProtocol을 준수하기 때문에

AttackerProtocol타입을 가지는 attacker와 DefenceProtocol타입을 따르는 defencer에 들어갈 수 있게 된 겁니다.

class Player_B : AttackerProtocol , DefenceProtocol {
    
    //AttackerProtocol
    var attacker: AttackerProtocol?
    
	,,, 생략 ,,,
    
    init(name : String){
        self.name = name
        self.attacker = self
        self.defencer = self
    }

}

let playerA : Player_A = Player_A(name: "Minsu")
let playerB : Player_B = Player_B(name: "Chulsu")

playerA.attacker = playerB
playerA.attack() // Chulsu use Player_B Attack

그리고 Player_B도 마찬가지로 두개의 프로토콜을 준수 했기 때문에,

PlayerA의 attacker에 PlayerB가 들어갈 수 있게 되는 겁니다.

 

 - 인스턴스로 작동하는 타입으로 프로퍼티 -

class Player_A : AttackerProtocol , DefenceProtocol {
    
    //AttackerProtocol
    var attacker: AttackerProtocol?
    
    func attack() {
        guard let att = self.attacker else {
            print("\(self.name) attacker is nil")
            return}
        
        att.attackWay()
    }
    
    func attackWay() {
        print("\(self.name) use Player_A Attack")
    }
    
    //DefenceProtocol
    
    var name: String
    
    ,,, 생략 ,,,
    
}


class Player_B : AttackerProtocol , DefenceProtocol {
    
    //AttackerProtocol
    var attacker: AttackerProtocol?
    
    func attack() {
        guard let att = self.attacker else {
            print("\(self.name) attacker is nil")
            return}
        
        att.attackWay()
    }
    
    func attackWay() {
        print("\(self.name) use Player_B Attack")
    }
    
    //DefenceProtocol
    
    var name: String
    
 	,,, 생략 ,,,
    
}    

Player_A와 Player_B 모두 AttackerProtocol을 준수하기 때문에,

둘다 attacker와 attack() , attackWay() 이렇게 두개의 기능을 가지고 있습니다.

그리고 Player_A와 Player_B는 attackWay()의 내용이 다릅니다.

let playerA : Player_A = Player_A(name: "Minsu")
let playerB : Player_B = Player_B(name: "Chulsu")

playerA.attack() // Minsu use Player_A Attack
playerB.attack() // Chulsu use Player_B Attack

playerA.attacker = playerB
playerA.attack() // Chulsu use Player_B Attack

그리고 위에 처럼 해주면 처음에는 PlayerA는 Minsu로 나오고 PlayerB는 Chulsu로 나오는데,

playerA.attacker를 철수인 PlayerB로 바꿔주니 결과가 철수로 나왔습니다.

 

이것을 통해 타입으로서 프로토콜을 가지게 되면, 다양한 인스턴스를 프로퍼티에 넣고

다양한 기능을 구현 할 수 있게 됩니다.

물론 아래 처럼 AttackerProtocol을 제거하고 프로퍼티 타입만 지정해줘서 아래 처럼 사용 해줘도 됩니다.

class Player_A : DefenceProtocol {
    
    //AttackerProtocol
    var attacker: AttackerProtocol?
	
    ,,, 생략 ,,,
}

playerA.attacker?.attack() // Chulsu use Player_B Attack

그래서 결국 하고 싶은 말은,

프로퍼티를 프로토콜로 해준다면 특정 인스턴스만 받는 것이 아니라 프로토콜을 준수하는 다양한 인스턴스를 받을 수 있기 때문에,

다양한 기능을 사용 할 수 있다는 겁니다.

 

** 프로토콜을 따르는 get 프로퍼티와 타입 프로토콜 만들기 ** 

protocol ReceiveableProtocol{
    func received(data : Any, from : SendableProtocol)
}

protocol SendableProtocol{
    
    //SendableProtocol을 준수하는 인스턴스가 들어 올 수 있음
    var from : SendableProtocol {get}
    //ReceiveableProtocol을 준수하는 인스턴스가 들어 올 수 있음 - class나 Struct, enum만 가능할 줄 알았는데 Protocol도 가능ㅎ
    var to : ReceiveableProtocol? {get}
    
    func send(data: Any)
    
    //static class 모두 타입 프로퍼티인데
    //static은 상속이 불가능하고 //class는 상속이 가능하다.
    //하지만 프로토콜에서는 static으로 일단 해줘야한다.
    static func isSendableInstance(_ instance : Any) -> Bool
    
}

class Message : SendableProtocol , ReceiveableProtocol {
    
    var name : String = "Message"
    
    var from: SendableProtocol {
        return self
    }
    
    var to: ReceiveableProtocol?
    
    func send(data: Any) {
        print("Message send")
        guard let receiver : ReceiveableProtocol = self.to else {
            print("Message has no receiver")
            return
        }
        
        receiver.received(data: data, from: self.from)
    }
    
    static func isSendableInstance(_ instance: Any) -> Bool {
        
        if let sendableInstance : SendableProtocol = instance as? SendableProtocol {
            // nil 이면 true 아니면 false
            return sendableInstance.to != nil
        }
        
        return false
    }
    
    func received(data: Any, from: SendableProtocol) {
        print("Message received \(data) from \(from)")
    }
    
}

class Mail : SendableProtocol , ReceiveableProtocol {
    
    //이렇게 {return}을 해주면 get-only 프로퍼티 프로토콜이 된다.
    var from : SendableProtocol{
        return self
    }
    
    var what : SendableProtocol?
    
    var to : ReceiveableProtocol?
    
    func send(data: Any) {
        
        print("Mail send")
        guard let receiver : ReceiveableProtocol = self.to else {
            print("Mail has no receiver")
            return
        }
        
        receiver.received(data: data, from: self.from)
        
    }
    
    func received(data: Any, from: SendableProtocol) {
        print("Mail received \(data) from \(from)")
    }
    
    class func isSendableInstance(_ instance: Any) -> Bool {
        if let snedableInstance : SendableProtocol = instance as? SendableProtocol {
            // nil 이면 true 아니면 false
            return snedableInstance.to != nil
        }
        
        return false
    }
    
}

보통 프로토콜 내부에 있는 프로퍼티에 {get} 이렇게 해도 

반환과 수정이 모두 됩니다.

하지만 만약 반환(get)만 하고 싶을 경우에는 아래 처럼 해주면 됩니다.

class Mail : SendableProtocol , ReceiveableProtocol {
    
    //이렇게 {return}을 해주면 get-only 프로퍼티 프로토콜이 된다.
    var from : SendableProtocol{
        return self
    }
    
    ,,, 생략 ,,,

}

 

- 타입 프로토콜 만들기 - 

protocol SendableProtocol{
    
    //SendableProtocol을 준수하는 인스턴스가 들어 올 수 있음
    var from : SendableProtocol {get}
    //ReceiveableProtocol을 준수하는 인스턴스가 들어 올 수 있음 - class나 Struct, enum만 가능할 줄 알았는데 Protocol도 가능ㅎ
    var to : ReceiveableProtocol? {get}
    
    func send(data: Any)
    
    //static class 모두 타입 프로퍼티인데
    //static은 상속이 불가능하고 //class는 상속이 가능하다.
    //하지만 프로토콜에서는 static으로 일단 해줘야한다.
    static func isSendableInstance(_ instance : Any) -> Bool
    
}

SendableProtocol을 보면 isSendableInstacne가 static으로 되어 있습니다.

그래서 타입 메소드로서 역할을 하도록 하는 겁니다.

class Message : SendableProtocol , ReceiveableProtocol {
    
    ,,, 생략 ,,,
    
    static func isSendableInstance(_ instance: Any) -> Bool {
        
        if let sendableInstance : SendableProtocol = instance as? SendableProtocol {
            // nil 이면 true 아니면 false
            return sendableInstance.to != nil
        }
        
        return false
    }
    
    ,,, 생략 ,,,
    
}

class Mail : SendableProtocol , ReceiveableProtocol {
    
   ,,, 생략 ,,,
   
    class func isSendableInstance(_ instance: Any) -> Bool {
        if let snedableInstance : SendableProtocol = instance as? SendableProtocol {
            // nil 이면 true 아니면 false
            return snedableInstance.to != nil
        }
        
        return false
    }
    
}

그리고 위와 같이 코드를 작성 해주면 됩니다.

 

잠깐!! 여기서 static과 class의 차이에 대해서 설명하도록 하겠습니다.

둘 다 타입으로서 역할을 가지지만 static은 상속이 안되고 class는 상속이 됩니다.

2021/01/24 - [Xcode/Swift - PlayGround] - PlayGround) Class func 와 Static func의 차이가 머지?

 

PlayGround) Class func 와 Static func의 차이가 머지?

안녕하세요!! 후르륵짭짭입니다. 이번에는 Swift의 상속을 공부하면서 궁금했던 Class func와 Static func의 차이를 다뤄 볼려고 합니다! 최근에 여러가지 바쁜 일이 있어서 블로그 운영이 쫌 뜸해졌네

hururuek-chapchap.tistory.com

자세한 내용은 위에서 다뤘습니다.

그리고 아래 처럼 사용 할 수 있습니다.

// 왜냐하면 SendableProtocol을 준수하지 않았기 때문이다.
Message.isSendableInstance("Hello") // false

Message.isSendableInstance(myPhoneMessage) // myPhoneMessages는 to가 존재하기 때문에 true

Message.isSendableInstance(yourPhoneMessage) // yourPhoneMessage to가 존재하지 않기 때문에 false

Mail.isSendableInstance(myPhoneMessage) // true

Mail.isSendableInstance(myMail) // true

 

** 가변 메소드 요구 프로토콜 ** 

protocol ResettableProtocol{
    //Mutating이 들어간 것으로 보아 내부 프로퍼티를 변경 할 것이라는 걸 암
    mutating func reset()
}

class Person : ResettableProtocol{
    
    var name : String?
    var age : Int?
    
    //class는 내부 프로퍼티를 변경 할때, 굳이 mutating이 필요가 없다.
    func reset() {
        self.name = nil
        self.age = nil
    }
    
}

struct Point : ResettableProtocol{
    var x : Int  = 0
    var y : Int = 0
    
    //반면에 struct는 값 타입이기 때문에 이렇게 mutating을 해줘야 한다.
    mutating func reset() {
        self.x = 0
        self.y = 0
    }
}

enum Direction : ResettableProtocol {
    case east , west , south, north, unkown
    
    //Enum도 마찮가지이다. 값 타입이기 때문에 Mutating을 해줘야 한다.
    mutating func reset() {
        self = Direction.unkown
    }
}

가변 메소드 프로토콜은 별거 아닙니다 ㅎㅎㅎ

Struct 구조체에 저장 프로퍼티를 메소드로 변경하려고 하면 mutating을 적어줘야합니다.

바로 이렇게 인스턴스에 있는 저장 프로퍼티를 함수로 변경 하는 것이 필요 할 때, 

프로토콜의 함수 앞에 mutating을 적어 주면 됩니다.

 

** 인니셜라이져 요구 프로토폴 **

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

struct Pet : Named{
    var name : String
    
    //구조체는 상속 할 수 없기 때문에 이니셜라이저 요구에 큰 상관없이 사용해주면 된다.
    init(name : String){
        self.name = name
    }
    
}

let pet = Pet(name: "Name - pet")
pet.name

class Person2 : Named{
    
    var name : String
    
    //하지만 클래스일 경우에는 상속을 받을 수도 있기 때문에
    //required가 들어가게 된다.
    required init(name: String) {
        self.name = name
    }
    
}

final class Person3 : Named{
    var name : String
    //하지만 클래스가 이렇게 상속 받을 수 없는 final로 되어 있다면
    //required가 붙지 않는다.
    init(name: String) {
        self.name = name
    }
}

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

class B : A , Named{
    
    //Initializer requirement 'init(name:)' can only be satisfied by a 'required' initializer in non-final class 'B'
    //이렇게 상속 받은 곳에 프로토콜에서 정의 한 것과 동일한 init()이 있다면
    // 위와 같은 오류가 발생한다.
    //왜냐하면 중복이 되었으니깐, 그래서 required override를 사용해서
    //프로토콜을 준수하는 동시에 A 클래스를 override 해주도록 해야한다.
    required override init(name: String) {
        super.init(name: name)
    }
}

프로토콜로 이니셜라이져 요구사항도 만들 수 있습니다.

일단 구조체는 상속이 안되기 때문에 아래 처럼 단순히 init()만 해주면 됩니다.

struct Pet : Named{
    var name : String
    
    //구조체는 상속 할 수 없기 때문에 이니셜라이저 요구에 큰 상관없이 사용해주면 된다.
    init(name : String){
        self.name = name
    }
    
}

또 final 붙이면 상속이 안되기 때문에 final class일 경우에는 그냥 해주면 됩니다.

 

하지만 위와 같은 경우가 아니라면 

class Person2 : Named{
    
    var name : String
    
    //하지만 클래스일 경우에는 상속을 받을 수도 있기 때문에
    //required가 들어가게 된다.
    required init(name: String) {
        self.name = name
    }
    
}

이렇게 required init()을 해줘야합니다.

 

만약에 상속 받는 곳에도 init()이 있고 프로토콜에도 init()이 있다면 어떻게 해야하나!

그럴 때는 아래 처럼 required override 해주면 됩니다.

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

class B : A , Named{
    
    //Initializer requirement 'init(name:)' can only be satisfied by a 'required' initializer in non-final class 'B'
    //이렇게 상속 받은 곳에 프로토콜에서 정의 한 것과 동일한 init()이 있다면
    // 위와 같은 오류가 발생한다.
    //왜냐하면 중복이 되었으니깐, 그래서 required override를 사용해서
    //프로토콜을 준수하는 동시에 A 클래스를 override 해주도록 해야한다.
    required override init(name: String) {
        super.init(name: name)
    }
}

 

** 프로토콜 조합 **

protocol one {
    var one : String {get}
}

protocol two {
    var two : Int {get}
}

struct Ship : one , two {
    var one : String
    var two : Int
}

struct airPlane : one{
    var one : String
}

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

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

다음과 같이 프로토콜 one 과 two가 있습니다.

그리고 각 인스턴스 마다 프로토콜을 다르게 받았습니다.

또한 함수도 만들어 줬습니다.

//조합을 하게 된다면 아래 처럼 접근이 가능하다.
func celebrateBirthDay(to celebrator  : one & two){
    //one & two 모두 준수하기 녀석이 들어오기 때문에 아래 처럼 각 부분에 접근이 가능하다.
    print("Happty BirthDay \(celebrator.one)!! \n Now you are \(celebrator.two)")
}

이렇게 one & two를 하게 된다면 

프로토콜 one을 준수하면서 two  모두 준수하는 인스턴스만 들어 올 수 있다는 의미 입니다.

그래서 아래 같이 Ship은 one과 two를 모두 준수하기 때문에 사용이 됩니다.

let yahagom : Ship = Ship(one: "yahagom", two: 99)
//이렇게 yahagom은 Ship 자체가 one two 프로토콜을 모두 상속 받기 때문에 사용 될 수 있다.
celebrateBirthDay(to: yahagom)

반면 Car는 one만 준수해서 안 됩니다.

let myCar : Car = Car(name: "벤츠")
//celebrateBirthDay(to: myCar) // 반면 myCar는 two를 준수하지 않았기 때문에 접근 할 수 없다.

 

또한 변수에다가 클래스와 프로토콜 조합을 하는 것도 가능 합니다.

//그리고 아래 처럼 클래스와 프로토콜 조합도 가능하다.
//이렇게 했을 경우에는 Car 타입이면서 two를 준수하는 인스턴스가 와야한다.
//그래서 Car를 상속 받고 있으면서도 two를 준수하고 있는 Truck은 가능하다.
var classAndProtocol : Car & two = Truck(name: "truck", age: 99)

이렇게 Car 타입이면서 two 프로토콜을 준수하는 것만 들어 올 수 있도록 변수나 프로퍼티 생성도 가능합니다.

 

하지만 당연히 클래스와 프로토콜 조합에서 클래스는 당연히 하나여야 합니다.

두개면 이상한 타입이기 때문에 안 됩니다. 

아래와 같이 클래스가 2개 이상인 것은 안됩니다.

//하지만 클래스 타입은 한 타입만 가능하다.
//Protocol-constrained type cannot contain class 'Infomation' because it already contains class 'Car'
//그래서 이미 Car 클래스를 포함하고 있기 때문에 Infomation은 들어 올 수 없다고 하는 것이다.
var twoClassAndProtocol : Car & Infomation & two

 

** 선택적 프로토콜 **

@objc protocol Moveable {
    func walk()
    //선택적인 프로토콜은 이렇게 해준다.
    @objc optional func fly()
}

class Tiger : Moveable {
    
    func walk() {
        print("Tiger walk")
    }
    
}

class Bird : Moveable {
    func walk() {
        print("Bird Walk")
    }
    
    func fly(){
        print("Bird Flys")
    }
}

선택적 프로토콜 앞에는 @objc를 해줘야합니다.

그리고 선택적을 받을 함수나 프로퍼티에는 @objc optional을 넣어줍니다.

이렇게 하면 위에 처럼 Tiger는 fly() 함수를 안 받을 수가 있습니다.

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

tiger.walk()
bird.walk()

bird.fly()

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

movableInstance = bird
movableInstance.fly?()

그리고 위에 처럼 사용 할 수가 있습니다!

 

** 프로토콜 Extension ** 

protocol extensionProtocol{
    var name : String {get set}
}

extension extensionProtocol{
    
    func function(){
        hide()
    }
    
    private func hide(){
        print("Hello")
    }
    
}


class Test : extensionProtocol{
    var name: String = "name is test"
    
    func function() {
        print("\(self.name) Hello world")
    }

    
}

프로토콜도 Extension이 가능합니다.

하지만 프로토콜 Extension은 규칙을 만들어 주는 것이 아니라 특정 기능을 그냥에 넣어주는 겁니다.

그냥 일반적은 클래스나 구조체 Enum의 Extension과 같다고 생각하면 됩니다.

위에 처럼 protocol에는 단순히 함수 하나면 적어줬지만,

Extension에서는 함수의 기능까지 적어줬습니다.

따라서 프로토콜을 준수하는 인스턴스는 Extension 내의 있는 기능을 그대로 사용 할 수 있습니다.

그리고 만약에 인스턴스에서 동일한 이름의 함수를 적는다면 프로토콜의 Extension에 적은 함수는 사용 할 수 없게 됩니다.

사용 방법은 간단합니다.

let test = Test()

test.function()

이렇게 바로 고유의 기능을 사용 할 수 있게 됩니다.

 

 

참고 사이트 : 

 - Protocol Extension

www.codingexplorer.com/protocol-extensions-in-swift-2/

 

Protocol Extensions in Swift — Coding Explorer Blog

Extensions let you add methods to existing types like classes, structs and enums. Swift 2 brings that capability to protocols with protocol extensions.

www.codingexplorer.com

 - Protocol의 정리 

minsone.github.io/mac/ios/swift-protocols-summary

 

[Swift]Protocols 정리

프로토콜(Protocols) 프로토콜은 메소드, 속성 그리고 다른 특정 작업 또는 기능의 부분에 맞는 요구 사항의 청사진을 정의한다. 프로토콜은 실제로 이들 요구사항 구현을 제공하지 않는다. - 구현

minsone.github.io

 - Protocol Private 사용 방법 

stackoverflow.com/questions/42585775/swift-declaring-private-functions-in-internal-protocol

 

Swift. Declaring private functions in internal protocol

How can I achieve something like this (doesn't compile): internal protocol InternalPrivateMix { private func doPrivately() internal func doInternaly() } Basically I want to kind of make a

stackoverflow.com

 

728x90
반응형

댓글