Search

[Swift 공식 문서] 22. Protocols

부제
카테고리
Swift
세부 카테고리
공식문서 번역
Combine 카테고리
최종편집일
2022/07/16 15:22
작성중
관련된 포스팅
생성 일시
2022/07/16 14:47
태그
목차
프로토콜이란 메서드, 프로퍼티와 같이 특정 작업 또는 기능에 필요한 요구사항들을 정의한 청사진입니다. 프로토콜은 다음에 적용이 가능합니다.
class
structure
enumeration
프로토콜을 채택함으로써 요구사항에 대한 실질적인 구현을 제공할 수 있습니다.
프로토콜의 요구사항을 만족하면, 프로토콜을 ‘conform’(준수) 했다고 표현합니다.
필수 구현사항에 대한 준수 여부를 구체화 하는 것에 더불어, 프로토콜을 확장하여 이러한 요구 사항 중 일부를 구현하거나 준수하는 타입이 활용할 수 있는 추가 기능을 구현할 수 있습니다.

Protocol Syntax

클래스, 구조체, 열거형과 비슷한 방식으로 프로토콜을 정의합니다.
protocol SomeProtocol { // protocol definition goes here }
Swift
복사
커스텀 타입이 특정 프로토콜을 채택하게 하려면, 프로토콜의 이름을 타입의 이름뒤에 콜론과 함께 작성하면 됩니다.
struct SomeStructure: FirstProtocol, AnotherProtocol { // structure definition goes here }
Swift
복사
클래스가 superclass 를 가진다면 superclass 의 이름을 프로토콜의 이름 앞에 작성하세요
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol { // class definition goes here }
Swift
복사

Property Requirements

프로토콜은 구체적인 이름과 타입이 정해져있는
타입 프로퍼티
인스턴스 프로퍼티 등을 요구할 수 있습니다.
단, 해당 프로퍼티가 계산 프로퍼티인지 저장 프로퍼티인지를 구체적으로 명시하지는 않는다.
프로토콜은 해당 프로퍼티가
gettable 인지
gettable 하면서 settable 한지는
구체적으로 명시할 수 있다.
프로토콜이 gettable 하면서 settable 한 프로퍼티를 요구하면, 상수 저장 프로퍼티 또는 read-only 한 계산 프로퍼티로는 해당 프로토콜의 요구사항을 충족할 수 없습니다. Gettable하면서 settable 한 프로퍼티는 { get set } 을 프로퍼티의 타입 뒤에 작성하여 나타냅니다.
protocol SomeProtocol { var mustBeSettable: Int { get set } var doesNotNeedToBeSettable: Int { get } }
Swift
복사
프로토콜에 타입 프로퍼티를 정의할 때는 항상 static 키워드를 맨 앞에 작성해줍니다.
protocol AnotherProtocol { static var someTypeProperty: Int { get set } }
Swift
복사
아래는 인스턴스 프로퍼티에 대한 예시입니다.
protocol FullyNamed { var fullName: String { get } }
Swift
복사
struct Person: FullyNamed { var fullName: String } let john = Person(fullName: "John Appleseed") // john.fullName is "John Appleseed"
Swift
복사
각각의 Person 의 인스턴스는 fullName 이라는 하나의 저장프로퍼티를 가진다.
이는 FullyNamed 라는 프로토콜의 요구사항으로 프로토콜을 완벽하게 준수하였다.
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"
Swift
복사
이 클래스에서는 요구사항인 fullName 을 read-only 한 계산 프로퍼티로 충족했다.
Starship 인스턴스는 의무적인 name 과 optional 한 prefix 를 가진다..
fullName 프로퍼티는 prefix 가 존재하면 이를 이용하여 전체 이름을 연장해 starship 의 풀 네임을 리턴한다.

Method Requirements

프로토콜은 또한
인스턴스 메서드
타입 메서드
을 요구사항으로 가질 수 있다.
중괄호없이 프로토콜 내에 정의하여 사용이 가능하다.
가변길이의 파라미터도 받을 수 있고. 대부분은 일반적인 메서드의 규칙과 동일하다.
하지만 프로토콜 정의부에서는 메서드의 파라미터는 초기값을 설정할 수 없다.
항상 static 키워드를 사용해서 타입 메서드를 정의해야 한다..
protocol SomeProtocol { static func someTypeMethod() }
Swift
복사
protocol RandomNumberGenerator { func random() -> Double }
Swift
복사
RandomNumberGenerator 프로토콜을 준수하는 모든 타입은 , random() 이라는 메서드를 구현해야합니다. 이 프로토콜은 어떻게 랜덤한 수를 만들어 낼지에 대해서는 어떠한 가이드라인도 제공하지 않습니다.
아래는 이 프로토콜을 준수하는 클래스이다. 이 클래스에서는 pseudorandom 수를 만드는 알고리즘을 구현했다.
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"
Swift
복사

Mutating Method Requirements

메서드가 자신이 속한 인스턴스를 수정할 필요가 있을 수 있습니다.
value 타입(structure, enumeration)의 인스턴스 메서드에는 mutating 키워드를 사용해 인스턴스의 다른 프로퍼티들에 접근할 권한을 부여할 수 있었습니다.
프로토콜을 채택한 모든 타입의 인스턴스를 수정할 수 있는 인스턴스 메서드를 프로토콜에 정의하고자 하는 경우 mutating 키워드를 프로토콜의 정의부에 작성해주면 됩니다.
class 의 메서드에는 mutating 키워드를 작성할 필요가 없습니다. mutating 키워드는 구조체 및 열거형에만 사용됩니다.
protocol Togglable { mutating func toggle() }
Swift
복사
위 예시에서 toggle()Togglable 을 준수하는 타입의 인스턴스를 수정할 의향이 있음을 mutating 을 통해 나타내었습니다.
이 프로토콜을 구조체 또는 열거형에 채택해 준수하고자 하는 경우에도 구조체, 열거형의 함수 정의부에 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
Swift
복사

Initializer Requirements

프로토콜은 준수하는 타입에 구현되어야할 명시적인 생성자를 요구할 수도 있습니다.
프로토콜의 정의부에 생성자를 작성하되, 중괄호를 포함한 생성자의 body 는 작성하지 않습니다.
protocol SomeProtocol { init(someParameter: Int) }
Swift
복사

Class Implementations of Protocol Initializer Requirements

프로토콜에서 요구하는 생성자를 구현할 때, 이를 지정 생성자 또는 편의 생성자로 구현해 프로토콜을 준수할 수 있습니다. 두가지 경우 모두 생성자의 구현 앞에 required 키워드를 작성해주어야 합니다.
class SomeClass: SomeProtocol { // 프로토콜을 준수하는 생성자 구현시 required 필수 작성 >> 하위 클래스도 프로토콜을 준수함을 나타내기 위함. required init(someParameter: Int) { // initializer implementation goes here } }
Swift
복사
required 사용에 따라, 프로토콜을 준수하는 클래스를 상속받은 클래스도, 해당 프로토콜을 준수함을 명시적으로 나타내게 됩니다.
final class 의 경우 프로토콜의 생성자 구현시 required 를 작성하지 않아도 됩니다. final 클래스는 상속이 불가하기 때문입니다.
하위 클래스가 상위 클래스의 지정 생성자를 override 하는 경우, 프로토콜의 요구사항에 해당하는 생성자를 구현하는 경우, 생성자의 구현 앞에 requiredoverride 를 함께 작성해줍니다.
protocol SomeProtocol { init() } class SomeSuperClass { init() { // initializer implementation goes here } } class SomeSubClass: SomeSuperClass, SomeProtocol { // required -> 상속 시 프로토콜 준수 여부의 명확성을 위해 작성 // override -> 상위 클래스의 지정생성자와 동일한 형태 이므로 required override init() { // initializer implementation goes here } }
Swift
복사

Failable Initializer Requirements

프로토콜은 실패가능한 생성자도 요구할 수 있습니다.
failable 한 생성자를 요구하는 경우, 이를 failable 또는 non failable 생성자를 구현함으로서 프로토콜을 준수할 수 있습니다.
다만 non failable 한 생성자를 요구하는 경우, non failable 생성자를 구현해야하만 합니다.

Protocols as Types

프로토콜은 실제로 모든 기능을 스스로 구현하지 않습니다. 그럼에도 불구하고 프로토콜을 온전한 타입으로 사용이 가능합니다. 프로토콜을 타입으로서 사용하는 것을 existential 타입이라고 부릅니다.
이경우, 해당 타입은 해당 프로토콜을 준수하는 모든 타입을 의미하게 됩니다.
다른 타입의 사용이 가능한 곳에 프로토콜을 사용할 수 있습니다.
함수, 메서드, 생성자의 파라미터 또는 리턴 타입
상수 변수 또는 프로퍼티의 타입
어레이, 딕셔너리, 또는 다른 컨테이너의 항목들의 타입
프로토콜은 타입이기 때문에 그들의 이름의 첫글자는 대문자로 작성해주어야 합니다.
class Dice { let sides: Int let generator: RandomNumberGenerator init(sides: Int, generator: RandomNumberGenerator) { self.sides = sides self.generator = generator } func roll() -> Int { return Int(generator.random() * Double(sides)) + 1 } }
Swift
복사
위 예제에서는 n 개의 면을 가지는 주사위를 모델링한 Dice 라는 class 를 구현했습니다.
generator 프로퍼티는 랜덤 숫자를 만드는 생성기를 의미합니다.
generator 프로퍼티의 타입이 RandomNumberGenerator 임을 볼 수 있는데, 이는 프로토콜이다.
따라서 generator 프로퍼티에는 RandomNumberGenerator 를 준수하는 모든 타입의 값을 할당할 수 있다.
다만, generator 의 타입이 RandomNumberGenerator 이므로 Dice class 의 내부는 RandomNumberGenerator 프로토콜이 준수하는 모든 인스턴스에 적용되는 방식으로만 generator 를 사용할 수 있습니다.
즉, generator 의 기본 타입에 정의된 메서드나 프로퍼티는 사용이 불가능합니다.
하지만 protocol 타입을 기본타입으로 다운 캐스트 하게 되면 사용이 가능합니다.
Dice 는 또한 초기 상태를 설정하는 생성자를 갖고 있습니다. 이 생성자는 generator 라는 파라미터를 가지며, 이의 타입 또한 RandomNumberGenerator 입니다. RandomNumberGenerator 을 준수하는 모든 타입의 값을 해당 파라미터로 건내주어 새 Dice 인스턴스를 생성할 수 있습니다.
Diceroll 이라는 인스턴스메서드를 제공합니다. 이 메서드는 1과 주사위의 면의 개수 사이의 정수 값을 리턴합니다. 이메서드는 generatorrandom() 메서드를 호출하여서 0.0 ~ 1.0 사이의 새 랜덤 숫자를 생성합니다. 이렇게 생성된 랜덤 숫자는 주사위의 면의 값에 맞는 수로 변환되어 사용됩니다. generatorRandomNumberGenerator 을 채택하므로 항상 random() 메서드를 호출할 수 있도록 보장받습니다.
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator()) for _ in 1...5 { print("Random dice roll is \(d6.roll())") } // Random dice roll is 3 // Random dice roll is 5 // Random dice roll is 4 // Random dice roll is 5 // Random dice roll is 4
Swift
복사

Delegation

Delegation 이란 클래스나 구조체가 다른 타입의 인스턴스에게 몇가지 작업을 위임하는 디자인 패턴이다. 이 디자인 패턴은 위임하는 작업의 목록을 캡슐화하는 프로토콜을 정의함으로써 구현할 수 있다. 프로토콜을 채택함으로써 위임받을 작업의 구현을 보장할 수 있다.
Delegation 은
특정 action 에 반응하는 경우
외부 소스로부터 데이터 불러오는 경우
등에 사용된다.
protocol DiceGame { var dice: Dice { get } func play() } protocol DiceGameDelegate: AnyObject { // 클래스 only -> 약한 참조로만 사용됨 func gameDidStart(_ game: DiceGame) func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) func gameDidEnd(_ game: DiceGame) }
Swift
복사
DiceGame 프로토콜은 주사위를 사용하는 모든 게임에 채택 가능한 프로토콜입니다
DiceGameDelegateDiceGame 의 진행상황을 추적하는데 채택되는 프로토콜입니다. Strong reference cycle 을 방지하기위해 delegateweak 참조로 선언되어있습니다.
프로토콜을 AnyObject 를 사용해 class only 로 선언함으로써 SnakesAndLadders 클래스가 delegate 를 반드시 weak reference 로 사용하도록 합니다.
class SnakesAndLadders: DiceGame { let finalSquare = 25 let dice = Dice(sides: 6, generator: LinearCongruentialGenerator()) var square = 0 var board: [Int] init() { board = Array(repeating: 0, count: finalSquare + 1) board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02 board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08 } weak var delegate: DiceGameDelegate? func play() { square = 0 delegate?.gameDidStart(self) gameLoop: while square != finalSquare { let diceRoll = dice.roll() delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll) switch square + diceRoll { case finalSquare: break gameLoop case let newSquare where newSquare > finalSquare: continue gameLoop default: square += diceRoll square += board[square] } } delegate?.gameDidEnd(self) } }
Swift
복사
위 게임은 SnakesAndLadders 라는 클래스로 싸여져 있고 이 클래스는 DiceGame 프로토콜을 채택했다. 프로토콜을 준수하기 위해 gettable 프로퍼티인 diceplay() 메서드를 구현했다. ( 이 경우 dice 프로퍼티는 상수 프로퍼티로 선언되었는데, 이는 생성이후 변경할 필요가 없을 뿐더러 프로토콜이 gettable 한 프로퍼티를 요구하기 떄문이다.
Snakes and Ladders 게임의 보드는 클래스의 init() 생성자 내부에서 설정이 일어난다. 모든 게임 로직은 프로토콜의 play() 메서드로 옮겨진다. 이 play() 메서드에서는 dice 프로퍼티를 사용해서 주사위르 값을 제공한다.
delegate 프로퍼티가 optional 한 DiceGameDelegate 로 정의되어 있다. 이는 delegate 가 게임을 실행하는데 필수적이지 않기 때문이다. optional 타입이기 때문에 delegate 프로퍼티는 자동으로 nil 값으로 초기화된다. 그 후 사용자가 delegate 에 적합한 프로퍼티를 설정할 권한을 갖는다. DiceGameDelegate 가 class only 이기 때문에 delegateweak 로 선언에 reference cycle을 사전에 예방했다.
delegate 프로퍼티가 optional 한 DiceGameDelegate 이기 때문에, play() 메서드는 optional chaining 을 사용해서 delegate 의 메서드를 호출합니다. delegate 프로퍼티가 nil 이라면 delegate 메서드는 gracfully fail (Swift 에서 어떠한 에러도 없이 실패하는 것을 우아한 실패라고 표현하더군요) 하게 됩니다. delegate 프로퍼티 가 nil 이 아니라면, delegate 메서드가 호출되고 SnakesAndLadders 인스턴스가 파라미터로서 전달됩니다.
class DiceGameTracker: DiceGameDelegate { var numberOfTurns = 0 func gameDidStart(_ game: DiceGame) { numberOfTurns = 0 if game is SnakesAndLadders { print("Started a new game of Snakes and Ladders") } print("The game is using a \(game.dice.sides)-sided dice") } func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) { numberOfTurns += 1 print("Rolled a \(diceRoll)") } func gameDidEnd(_ game: DiceGame) { print("The game lasted for \(numberOfTurns) turns") } }
Swift
복사
DiceGameTrackerDiceGameDelegate 에 필요한 3개의 메서드를 모두 구현했습니다. 이 메서드를 사용해서 게임의 턴수를 계속 추적합니다. 게임이 시작되면 numberOfTurns 프로퍼티를 0으로 재설정하고, 새 턴이 시작될 때마다 증가시키며, 게임이 종료되면 총 턴수를 출력합니다.
gameDidStart(:) 를 보면 game 파라미터를 사용해서 실행되고 있는 게임에 관한 몇몇 정보를 출력합니다. game 파라미터는 DiceGame 이라는 타입을 가집니다. game 파라미터의 타입이 SnakesAndLadders 가 아니기 때문에 다른 프로퍼티나 메서드는 사용하지 못하고 , gameDidStart(:)DiceGame 이라는 프로토콜의 필요 조건에 해당하는 메서드와 프로퍼티만 사용 가능합니다. 그러나 메서드는 타입 캐스팅을 통해 SnakesAndLadders 타입인지 확인 후 해당 클래스의 프로퍼티와 메서드를 사용할 수도 있습니다. 위 예시에서는 game 프로퍼티가 SnakeAndLadders 인지 확인하고 그에 적절한 메세지를 출력하고 있습니다.
gameDidStart(_:) 메서드는 game 파라미터로 전달된 dice 프로퍼티에 접근합니다. gameDiceGame 프로토콜을 준수하는 것이 명백하므로 dice 프로퍼티의 존재가 보장됩니다. 따라서 gameDidStart(_:) 메서드는 실행되고있는 게임의 종류와는 상관없이 dicesides 프로퍼티에 접근할 수 있습니다.
let tracker = DiceGameTracker() let game = SnakesAndLadders() game.delegate = tracker game.play() // Started a new game of Snakes and Ladders // The game is using a 6-sided dice // Rolled a 3 // Rolled a 5 // Rolled a 4 // Rolled a 5 // The game lasted for 4 turns
Swift
복사

Adding Protocol Conformance with an Extension

이미 존재하는 타입이 새 프로토콜을 채택하고 준수하도록 확장할 수 있습니다. 이는 이미 존재하는 타입의 소스코드에 접근 권한이 없어도 가능합니다. Extension 에는 새 프로퍼티, 메서드, 첨자 를 추가할 수 있기에 프로토콜이 요구하는 사항을 충족할 수 있습니다.
인스턴스의 타입이 extension 부에서 프로토콜을 준수한다면 , 타입의 이미 존재하는 인스턴스도 자동으로 프로토콜을 채택하고 준수합니다.
예로, TextRepresentable 이라고 불리는 프로토콜은 text 로 표현 가능한 방법이 있는 모든 타입에 구현이 가능합니다. 이 text 는 자기 자신에 대한 설명일 수도 있고, 현재 상태에 대한 내용일 수 있습니다.
protocol TextRepresentable { var textualDescription: String { get } } extension Dice: TextRepresentable { var textualDescription: String { return "A \(sides)-sided dice" } }
Swift
복사
extension 에서 TextRepresentable 을 채택했습니다. 타입의 이름뒤에 콜론과 함께 작성해주면 됩니다. 이 후에 protocol 에서 요구하는 사항들을 extension의 괄호 내부에 작성해주면 됩니다.
이제 모든 Dice 의 인스턴스는 TextRepresentable 로서 다루어질 수 있습니다.
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator()) print(d12.textualDescription) // Prints "A 12-sided dice"
Swift
복사
유사하게 SnakesAndLadders 게임 클래스 또한 extension 에 TextRepresentable 프로토콜을 채택하고 준수할 수 있습니다.
extension SnakesAndLadders: TextRepresentable { var textualDescription: String { return "A game of Snakes and Ladders with \(finalSquare) squares" } } print(game.textualDescription) // Prints "A game of Snakes and Ladders with 25 squares"
Swift
복사

Conditionally Conforming to a Protocol

제네릭 타입의 경우 특정 상황에서만 프로토콜의 요구사항을 충족시킬 가능성이 있습니다. 제네릭 타입을 상황에 따라 가능한 경우에만 프로토콜을 준수하도록 할 수 있습니다. 이는 type 확장시 제약조건을 나열하면 됩니다. 제약조건을 프로토콜의 이름 옆에 작성하세요.
아래의 extension 은 Array 인스턴스가 TextRepresentable 을 준수하는 타입을 저장한다면 Array 인스턴스를 TextRepresentable 프로토콜을 준수하도록 만듭니다.
extension Array: TextRepresentable where Element: TextRepresentable { var textualDescription: String { let itemsAsText = self.map { $0.textualDescription } return "[" + itemsAsText.joined(separator: ", ") + "]" } } let myDice = [d6, d12] print(myDice.textualDescription) // Prints "[A 6-sided dice, A 12-sided dice]"
Swift
복사

Declaring Protocol Adoption with an Extension

타입이 이미 프로토콜의 모든 요구사항을 준수하지만 아직 프로토콜을 채택한다고 선언하지 않은 경우, 텅빈 extension 에 프로토콜을 명시하기만 하면 됩니다.
struct Hamster { var name: String var textualDescription: String { return "A hamster named \(name)" } } extension Hamster: TextRepresentable {}
Swift
복사
Hamster 의 인스턴스는 이제 TextRepresenatable 로서 사용이 가능합니다.
let simonTheHamster = Hamster(name: "Simon") let somethingTextRepresentable: TextRepresentable = simonTheHamster print(somethingTextRepresentable.textualDescription) // Prints "A hamster named Simon"
Swift
복사
프로토콜의 요구사항을 만족한다고 프로토콜을 자동으로 채택하지 않습니다. 프로토콜은 반드시 채택에 대한 선언이 분명하게 명시되어야합니다.