본문 바로가기
공부/[iOS&Swift]

[Swift] Protocol에 대하여

by 인생은아름다워 2022. 1. 24.

✏️ Swift의 Protocol에 대하여

Swift는 프로토콜 지향 프로그래밍을 선호하기(?)에 이번에 공부할 프로토콜에 대해서는 정확하게 이해해야 할 것 같다!

Protocol 이란?

공식문서에서는 이렇게 정의한다.

메서드, 프로퍼티, 또는 다른 요구사항들의 청사진 우리는 이 청사진이라는말을 또 사용한적이 있다.

Type (class, struct등)이 어떤 인스턴스의 청사진이었다면, 바로 이 타입의 청사진이 프로토콜인 것이다. 그리고 어떤 타입이 그 청사진의 요구사항을 따른다면, 그 프로토콜을 채택(adopt)한 것이며, 그 프토콜을 준수한다(conform)고 표현한다.

✏️ Protocol정의의 기본 형태와 채택

protocol [프로토콜 이름] {
    // 정의
}

위와 같은 형태로 정의할 수 있으며

class someClass: someProtocol {
    // someProtocol을 채택한 class
}

protocol someProtocol {
    // something
}

이 때 어떤 클래스가 다른 클래스를 상속받을 경우 상속받는 슈퍼클래스를 제일 앞에, 그리고 그 이후에 채택하는 프토토콜들을 나열해주면 된다.

swift에서는 다중상속이 불가능하지만, 프로토콜은 여러개의 프로토콜을 채택할 수 있다!

✏️ Protocol Requirements(프로토콜의 요구사항)

  • Property requirement(프로퍼티 요구사항)

프로토콜은 프로토콜을 채택한 타입에게 그 프로퍼티의 청사진을 제공할 수 있다. 즉, 어떤 프로퍼티를 꼭 만들라는 요구를 할 수 있다. 하지만, 그 프로퍼티를 프로토콜에서 직접 정의해주지는 않으며 어떤 종류(stored property or computed property)인지 신경쓰지 않는다. 단지 프로토콜은 프로퍼티의 Type과 이름(name)만 맞으면 되게끔프로퍼티를 구현하도록 요구한다.

프로토콜에서 프로퍼티 요구사항을 작성할 때는 꼭 Computed property의 형태  var키워드로 작성해야한다. (The protocol also specifies whether each property must be gettable or gettable and settable.)

gettable, settable로 정의했는데 상수(constant), 또는 읽기전용 연산프로퍼티로 사용할 수 없는 것이다.

protocol someProtocol {
    var mustBeSettable: Int { get set }
    // 상수 저장프로퍼티 또는 읽기전용 연산프로퍼티로 사용할 수 없다.

    var doesNotNeedToBeSettable: Int { get }
    // 어떤 프로퍼티로도 사용할 수 있다. (상수, 변수 / 저장, 연산프로퍼티)
    // 원한다면 setter를 추가하여 사용해도 된다.
}

아래 예시를 통해 gettable만 선언했지만, setter를 적용한 예시를 보자.

protocol Sendable {
    var from: String { get }
    var to: String { get }
    // 두 프로퍼티 모두 읽기전용으로 선언
}

class Message: Sendable {
    var sender: String
    var from: String {
        set {
            sender = "보낸사람:" + newValue
        }
        get {
            return self.sender
        }

    }
    var to: String

    init(sender: String, receiver: String) {
        self.sender = sender
        self.to = receiver
    }
}

var letter = Message(sender: "JM", receiver: "MJ")
print(letter.from, letter.to, letter.sender)
// JM MJ JM
letter.from = "JM~"
print(letter.from, letter.to, letter.sender)
// 보낸사람:JM~ MJ 보낸사람:JM~
}

static를 통해 타입 프로퍼티를 명세할 수도 있다. swift의 class의 경우, class, static키워드를 통해 각각 상속 가능/불가능한 타입 프로퍼티를 만들 수 있는데, 프로토콜에서는 모두 static 키워드를 사용해주면 된다!

  • Method requirement(메서드 요구사항)

프로퍼티와 마찬가지로, 프로토콜은 instance method & type method 또한 청사진을 제공할 수 있다. 다만 중괄호({})는 생갹하고, 매개변수와 반환의 타입은 반드시 명시해야 한다. 그리고 매개변수의 기본값을 지정할 수는 없다. 그리고 static키워드를 통해 타입 메서드를 지정할 수 있고, 실제 구현할때는 class/static 키워드 중 필요한 어느것을 사용해서 구현해도 무방하다.

protocol Receivable {
    func received(data: Any, from: Sendable) // Sendable protocol을 따르는 from을 매개변수
}

protocol Sendable {
    // 이렇게 프로토콜을 따른다는것을 타입처럼 표현할 수도 있다.
    // 이럴 경우 from / to 는 어떤 타입이더라도 각 프로토콜을 준수하는 프로퍼티이면 될다.
    var from: Sendable { get }
    var to: Receivable? { get }

    func send(data: Any)

    static func isSendable(_ instance: Any) -> Bool
}

class Message: Sendable, Receivable {
    // Sendable을 마치 타입처럼 사용
    var from: Sendable {
        return self
    }

    var to: Receivable?

    func send(data: Any) {
        guard let receiver = self.to else {
            print("MSG don't have receiver")
            return
        }
        receiver.received(data: data, from: self.from)
    }

    func received(data: Any, from: Sendable) {
        print("MSG received: \\\\(data) from \\\\(from)")
    }

    func isSendable(_ instance: Any) -> Bool {
        // 타입 캐스팅
        if let sendableInstance: Sendable = instance as? Sendable {
            return true
        }
        return false
    }
}

Protocol을 마치 Type처럼 사용한것을 잘 봐두면 좋을 것 같다!

  • Mutating Method requirement(가변 메서드 요구사항)

값타입(struct, enum)의 Instance method에서 내부의 값을 변경할 때는 mutating키워드를 지정해준 것과 같이, Protocol에서는 메서드에서 자신의 인스턴스의 값을 변경시킬 경우 mutating키워드를 명시해줘야한다.

이 프로토콜을

class가 채택한다면, mutating키워드를 무시하고 구현해도 되지만, struct와 enum은 그러면 안되겠지!?

  • Initializer requirement(생성자 요구사항)

Protocol은 Method를 요구하듯이 생성자(initializer)도 요구한다. 생성자도 결국 하나의 메서드니깐 당연히 그럴 수 있다. 그리고 문법 또한 method와 같다.

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

struct와 class의 차이를 보면, class의 생성자 앞에는  required라는 키워드가 붙어있다. 이유는 상속 때문인데, 이제 Person 타입을 상속하는 어떤 클래스 또한 Named 프로토콜을 준수해야하며, 그렇기 때문에 같은 형태의 생성자를 필요로한다. 이러한 내용 때문에 required 키워드를 붙여줘야한다.

다만, final을 통해 상속을 못 하도록 만들어둔 class의 경우 해당 키워드를 쓰지 않아도 된다!

아래 코드를 보자.

    protocol someProtocol {
        init(name: String)
    }

    class Person {
        var name: String

        init(name: String)
    }

    class Student: Person, someProtocol {
        var age: Int

        required override init(name: String) {
            super.init(name: name)
        }
    }

이렇게 required와, override 키워드 모두 필요한 상황 : 슈퍼클래스가 해당 프로토콜을 채택하지 않았는데, 서브클래스가 채택한 프로토콜의 생성자를 이미 가지고있는 경우 위의 상황에서는 그냥 두 키워드를 다 적어두면되고, 순서는 상관없다!

Protocol은 실패 가능한 생성자 또한 요구할 수 있는데, 이러한 프로토콜을 채택한 타입에서 실제 구현할 때는 실패 가능한 생성자가 아닌 일반 생성자로 구현해도 무방하다!

  • 프로토콜의 상속, 그리고 클래스 전용 프로토콜

Protocol은 상속이 가능하다. 클래스의 상속 문법과 유사하게 사용하면 된다.

protocol superProtocol {
    func read()
}

protocol subProtocol: class, superProtocol {
    func write()
}

class someClass: subProtocol {
    func read() {
        print("read")
    }
    func write() {
        print("write")
    }
}

// struct에서는 사용불가(class로 선언된 프로토콜을 채택)
// 아래 코드는 컴파일에러 발생
struct someStruct: subProtocol {
    func read() {
        print("read")
    }
    func write() {
        print("write")
    }
}

이렇게 프로토콜도 상속을 받을 수 있으며, class를 통해 클래스에서만 채택이 가능하도록 정의할 수도 있다.

  • Protocols as Types(타입으로서의 프로토콜)

프로토콜은 어떤 기능을 정확하게 구현하지는 않았지만 이것을 마치 하나의 Type인 것 처럼 코드에서 사용할 수 있다. 이것을 existential type(실존적 타입)이라고 부른다. 이것은 대량 이런 뜻으로 해석된다. -> 어떤(명시된) 프로토콜을 채택한 타입

protocol asType {
    ...
}

class Person: asType {
    ...
}
class Dog: asType {
    ...
}

var jm = Person()
var tiger = Dog()

var myMixedArray: [asType]

//이게 가능하다
myMixedArray = [jm, tiger]
1. 함수, 메서드, 생성자의 매개변수 또는 반환타입
2. 상수, 변수 또는 프로퍼티의 타입
3. Array, Dictionary등 컨테이너의 타입

위의 예시들에서 Protocol이 타입으로 사용할 수 있다.(그냥 그동안 사용했던 타입처럼 웬만하면 다 쓸 수 있다.) 그리고 프로토콜은 이렇게 타입으로 사용되니까 Swift의 다른 타입들 처럼 이름의 첫 문자를 대문자로 만들어준다.

다만 프로토콜을 타입으로 명시한 경우, 당연히 이것은 어떤 스위프트의 기본 타입이 아니기 때문에(Any 타입으로 할당된다 생각하자) 기본 타입의(underlying type) 프로퍼티, 메서드 등을 사용할 수는 없고 원한다면 Downcasting으로 프로토콜을 기본타입으로 캐스팅해줄 필요가 있다!

  • Delegation (위임)

공식문서에서는 위임을 이렇게 표현한다.

말 그대로 위임: 어떤 인스턴스의 책임(할 일)을 다른 타입의 인스턴스에게 '위임'하는 디자인 패턴 애플의 프레임워크에서는 이 디자인패턴을 매우 중요하기 여기고 있다. 그렇기 때문에 관련된 공식 문서나 포럼의 글들은 꼭 읽어봐야겠다.

애플프레임워크에는 보통 xxxDelegate라는 식의 이름으로 프로토콜을 정의해두고, xxx인스턴스의 일을 위임한다고 한다. 말을 좀 어렵게 적었지만 예를들자면, UITableView타입의 인스턴스가 해야하는 일을 위임받아서 처리하는 인스턴스는 UITableViewDelegate 프로토콜을 준수하면 된다! 라는 것!

🍎 참고자료

Yagom의 Swift Pramming 3판 

Swift공식문서 - Protocols

댓글