Swift

Swift 공식문서로 공부하기: Protocols

소재훈 2022. 4. 4. 03:18
 

Protocols — The Swift Programming Language (Swift 5.6)

Protocols A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of tho

docs.swift.org

 

위 공식문서를 보고 프로토콜에 대해서 공부해보자. 공부했지만 까먹은 Delegate 디자인 패턴을 더 깊이 공부할 수 있도록...ㅠㅠ

먼저 정의를 보면 이렇게 나와 있다.

 

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. 🤔

 

프로토콜은 메서드(methods), 프로퍼티(properties) 및 다른 기타요구사항에대한 청사진(Blueprint)를 제공한다고 한다! 정의만 읽어서는 자바의 인터페이스와 비슷해보인다.

 

프로토콜이 있으면 클래스, 구조체, 열거형 등에서 프로토콜을 채택해서 method등을 구현한다고 한다.

프로토콜의 요구사항을 충족한다면 그 클래스, 구조체, 열거형을

프로토콜을 준수한다.

라고 한다.

 

프로토콜에 명시되어있는 요구사항을 구현하는 것 외에도

프로토콜을 확장해서 요구사항의 일부만 구현하거나, 추가기능을 구현할 수도 있다.

 

📝 Protocol의 문법

그렇다면 프로토콜의 문법에 대해서 알아보자!😊

프로토콜의 문법은 클래스나 구조체, 열거형과 비슷하게 생겼다.

protocol SomeProtocol {
    // Protocol Definition
}

 

프로토콜은 다른 프로토콜을 상속받을 수 있고,

프로토콜 이름 다음에 콜론(:)으로 구분해서 다른 프로토콜의 이름을 쓴다.

다음과 같이 여러개의 프로토콜을 상속받을 수 있다.

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

 

프로토콜을 상속받는 클래스를 구현할 때, 

클래스에 슈퍼클래스(Super Class)가 있다면 슈퍼클래스를 먼저!

상속받은 다음에 그 후에 상속받을 프로토콜을 적어야한다.

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

 

📝 프로퍼티 요구사항

프로토콜은 인스턴스 프로퍼티 또는 타입프로퍼티를 요구할 수 있다.

프로토콜은 프로퍼티가 stored property인지 computed property인지를 지정하지 않고 필요한 프로퍼티의 이름과 타입만을 지정한다. 그리고 각각의 프로퍼티가 gettable한지, 아니면 gettable하면서 settable한지도 명시해야한다!

 

프로토콜이 gettable & settable한 프로퍼티를 요구할 때?

프로퍼티는 상수 저장 프로퍼티(constant stored property)나

읽기만 허용된 계산 프로퍼티(read-only compted property)가 될 수 없다.

 

gettalbe하거나 settable한 프로퍼티를 요구할 때?

-> stroed property이던 computed property이던 상관없다!

 

또 프로토콜의 프로퍼티는 반드시 var키워드와 함께 작성된 변수 이어야한다.

Gettable, Settable한 프로퍼티는 타입 선언 뒤에 { get set } 을 적어서 표현하고,

Gettable 프로퍼티는 { get } 으로 표현한다.

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

 

타입프로퍼티를 프로토콜에 정의할 때는 반드시 static 키워드가 와야한다.

이 요구사항은 클래스에서 프로토콜을 구현해서 앞에 class 나 static키워드를 붙일 수 

있는 상황에서도 지켜져야함에 명심하자.

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

 

다음 코드는 단일 인스턴스 프로퍼티를 요구하는 프로토콜의 코드입니다. 

한번 살펴볼까유

protocol FullyNamed {
    var fullName: String { get }
}

위에 보이는 FullyNamed 프로토콜은 fullName이라는 String타입의 이름 데이터를 원하고 있습니다.

또 fullName이라는 Gettable인스턴스도 정의되어 있네요 🤔

FullyNamed프로토콜을 상속받은 구조체나 클래스는 반드시 fullName이라는 이름의 Gettable인스턴스가 있어야합니다. 없으면 에러가 난다네요...깐깐...

 

그럼 FullyNamed 프로토콜을 상속받은 구조체 예시를 하나살펴볼까요?

struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

사람에 대한 정보를 나타내는 Person이라는 구조체가 정의되어 있습니다. 

위에서 배운대로 구조체의 이름 옆에 콜론(:)으로 구분되어 있고 프로토콜의 이름이 와서

FullyNamed 프로토콜을 구현한다고 표현했네요.

 

또 위에서 FullyNamed프로토콜을 상속받은 구조체는 fullName이라는 이름의

String타입의 인스턴스도 정의되어 있어야 한다고 했죠?

잘 정의되어 있는 것을 볼수 있네요. 😉

이것이 Person구조체가 상속받은 FullNamed프로토콜의 요구사항을 잘 만족했다는 것을 의미합니다. 😊

만약 요구사항을 만족하지 못한다면 Swift의 컴파일타임에 에러가 발생합니다...ㅠ

 

이번에는 FullyNamed프로토콜을 상속받은 조금복잡한 형태의

클래스를 살펴봅시다!

class Starship: FullyNamed {
    var prefix: String?
    var name: String
    init(name: String, prefix: String? = nil) {
        self.name = name
        self.prefix = prefix
    }
    var fullName: String {
        return (prefix != nil ? prefix! + " " : "") + name
    }
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"

Starship클래스는 프로토콜의 요구사항인 fullName을 계산 읽기전용 프로퍼티로 구현하고있네요.

각각의 Starship인스턴스는 String타입의 name과 옵셔널 String타입인 prefix프로퍼티를 가지네요

만약 prefix프로퍼티가 존재한다면 name의 앞에 prefix를 붙여서 인스턴스의 fullName프로퍼티로 사용하고 있습니다. 😑

 

📝 메서드 요구사항

프로토콜은 구현되어야 할 인스턴스 메서드를 요구합니다. 

정의는 다른 구조체나 클래스에서 메서드의 문법과 같지만 다른점이 있다면

메서드 바디, 중괄호 {} 가 없다는 점이네요.

가변 파라미터도 하용되고, 파라미터 기본값이 주어질 수 있는 등 일반 메서드와 조건이 똑같습니다. 하지만 프로토콜의 내부에서 메서드 매개변수는 지정할 수 없고  프로토콜을 구현할 때 지정할 수 있습니다.

 

프로퍼티의 요구사항과 마찬가지로 프로토콜에서 메서드를 정의할 경우에는 항상! static키워드를 메서드 앞에 붙여야 하니 주의합시다!!😊 만약에 static키워드를 붙이지 않고, 나중에 구조체나 클래스에서 구현할 때 static키워드를 붙인다고 해도 에러가 발생합니다!! 만약 프로토콜에서 구현해준다면 반드시 static 키워드를 붙입시다.

protocol SomeProtocol {
    static func someTypeMethod()
}

 

먼저 하나의 메서드만을 요구하는 프로토콜을 한번 살펴봅시다.

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator프로토콜은 random이라는 이름의 Double타입의 값을 반환하는 함수를 요구하고 있습니다. 지금 이 프로토콜에서는 구현되지 않았지만 프로토콜을 구현할 구조체나 클래스에서 구현하면 되는 것입니다!!

random 메서드의 의도가 임의의 0.0 ~ 1.0 사이의 수를 반환하는 것이라고 해봅시다.

 

RandomNumberGenerator프로토콜을 구현한 클래스를 한번살펴볼까요?

참고로 이 클래스는 팰린드롬 넘버를 생성하는 알고리즘을 구현하고 있습니다.

class LinearCongruentialGenerator: RandomNumberGenerator {
    var lastRandom = 42.0
    let m = 139968.0
    let a = 3877.0
    let c = 29573.0
    func random() -> Double {
        lastRandom = ((lastRandom * a + c)
            .truncatingRemainder(dividingBy:m))
        return lastRandom / m
    }
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"

 

📝Mutating Method Requirements

솔직히 Mutating을 어떻게 번역해야 자연스럽게 표현할 수 잇을 지 몰라서 그냥 공식문서에 있는대로 썼습니다. 😭

가끔 인스턴스가 가지고 있는 값을 메서드에서 수정(modify 또는 mutating)해야할 때가 있습니다. 그렇죠?

하지만 그냥 인스턴스의 값을 수정하면 에러가 발생합니다. 😱

이럴때는 메서드의 func 키워드앞에 mutating키워드를 붙이면 메서드 안에서 인스턴스가 가지고 있는 어떤 프로퍼티라도 수정할 수 있게 됩니다. 이것과 관련된 내용은 다음 문서에서 잘 설명되어 있네요. 시간되면 한번씩 읽어보세요😊

Modifying Value Types from Within Instance Methods.

 

 

만약 프로토콜을 구현하는 모든 인스턴스를 수정하려한다면 프로토콜 인스턴스 메서드 요구사항을 다음과 같이 정의할 수 있습니다.

바로 protocol의 앞에 mutating키워드를 붙이면 됩니다. 그러면 프로토콜을 구현하는 구조체와 열거형에서 요구사항을 충족할 수 있게됩니다. 😊

 

만약 프로토콜을 정의할 때 메서드 앞에 mutating키워드를 작성했다면, 클래스에서 프로토콜을 구현할 때는 mutating키워드를 작성하지 않아도 됩니다. mutating키워드는 구조체나 열거형에서 구현할 때 사용하면 됩니다.

 

아래의 예시는 Togglable이라는 하나의 인스턴스 메서드 toggle()를 정의하는 프로토콜을 구현하고 있습니다. 이름에서도 드러나듯이 toggle()메서드는 어떠한 타입의 상태를 바꾸는 기능을 할거에요. 

 

프로토콜을 구현하면서 구현된 메서드에서 인스턴스의 프로퍼티를 변경해야하기 때문에 프로토콜 정의에서 toggle()메서드는 mutating키워드가 붙어있습니다.

protocol Togglable {
    mutating func toggle()
}

만약 Togglable프로토콜이 구조체나 열거형에서 구현된다면 그 구조체나 열거형은 toggle()메서드를 구현할 때 mutating키워드로 표시할 수 있습니다.

 

아래 예시는 OnOffSwitch라는 열거형의 정의를 보여주고 있습니다. 

이 열거형은 열거형의 on, off라는 두가지 값으로 상태를 토글합니다.

그리고 Togglable 프로토콜의 요구사항을 맞추기 위해서 열거형의 toggle메서드 구현에서 mutating키워드가 붙어있는 것을 볼 수 있습니다.

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on