Swift/Swift 가이드

[Swift] 공식 문서 - 프로토콜(Protocols) - 1

유정주 2022. 7. 31. 20:37
반응형

* Protocols 가이드는 너무 길어서 두 편으로 나눠 포스팅합니다.

2편은 여기에서 볼 수 있습니다. 최하단에도 링크를 적어두었습니다.

 

새로 배운 점

  • 구현해야 하는 요구사항을 지정하는 것 이외에도 이러한 요구사항 중의 일부를 구현하거나 추가 기능을 구현하도록 프로토콜을 확장할 수도 있습니다.
  • Property 요구사항(requirements)은 항상 var 키워드를 앞에 붙여서 변수 프로퍼티로 선언합니다.
  • 프로토콜에서 메서드들은 메서드의 body와 괄호를 제외하고 나머지를 작성합니다. 
  • 프로토콜에서 메서드는 가변 매개변수는 허용하지만 기본값은 설정할 수 없습니다.
  • 프로토콜에 mutating을 사용한 경우, 이 프로토콜을 따르는 클래스를 구현할 때는 mutating을 명시하지 않아도 됩니다.
  • 프로토콜을 따르는 클래스에 desinated 이니셜라이저 또는 convenience 이니셜라이저로 프로토콜 이니셜라이저 요구사항을 구현할 수 있습니다. 이 경우에는 반드시 이니셜라이저 구현에 required 수식어를 붙여야 합니다.
  • required 수식어를 사용하면 이를 따르는 클래스의 모든 서브클래스에 명시적/암시적인 이니셜라이저 요구사항 구현을 제공하도록 할 수 있습니다.
  • 프로토콜은 실제로 기능 자체를 구현하지 않습니다. 그럼에도 프로토콜을 완전한 타입으로 사용할 수도 있습니다.
  • Delegate 디자인 패턴은 위임된 책임을 캡슐화하는 프로토콜을 정의하여 구현되며, 이를 통해 이 프로토콜을 따르는 타입이 위임된 기능을 제공하도록 보장합니다.

 

Protocols

프로토콜(Protocols)은 메서드, 프로퍼티, 특정 Task나 일부 기능에 적합한

다른 요구사항들의 상세한 계획(blueprint: 청사진)을 정의합니다.

프로토콜은 요구사항들이 실제로 구현되는 클래스, 구조체, 열거형에서 채택될 수 있습니다.

프로토콜의 요구사항을 만족하는 모든 타입을 "프로토콜을 따른다(conform)"고 합니다.

 

구현해야 하는 요구사항을 지정하는 것 이외에도

이러한 요구사항 중의 일부를 구현하거나

추가 기능을 구현하도록 프로토콜을 확장할 수도 있습니다.

 

Protocol Syntax

프로토콜은 클래스와 유사한 방법으로 정의합니다.

protocol SomeProtocol {
    // protocol definition goes here
}

특정 프로토콜을 따르는 커스텀 타입을 정의하려면 타입 이름 뒤에 콜론(:)을 붙이고

프로토콜 이름을 작성하면 됩니다.

콤마로 구분하여 여러 프로토콜을 채택할 수 있습니다.

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

만약 클래스가 슈퍼클래스라면 적용할 프로토콜을 작성하기 전에 슈퍼클래스를 나열합니다.

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

 

Property Requirements

프로토콜은 프로토콜을 따르는 타입에서 특정 이름과 타입의

인스턴스 프로퍼티나 타입 프로퍼티를 제공합니다.

하지만 이 타입이 저장 프로퍼티인지 연산 프로퍼티인지 지정하지는 않습니다.

오직 프로퍼티의 이름과 타입만 지정합니다.

또한, 이 프로퍼티가 gettable, settable 한지 지정해야 합니다.

 

만약 프로토콜이 프로퍼티에 대해 gettable 하면서 settable 하다고 지정한다면,

해당 프로퍼티는 상수(constant) 저장 프로퍼티나 read-only 연산 프로퍼티로 설정할 수 없습니다.

만약 프로퍼티가 gettable로만 지정된다면 모든 종류의 프로퍼티로 설정할 수 있습니다.

 

Property 요구사항(requirements)은 항상 var 키워드를 앞에 붙여서 변수 프로퍼티로 선언합니다.

gettable / settable은 타입 선언 뒤에 "{ get set }"을 작성하고 gettalbe은 "{ get }"을 작성합니다.

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

 

프로토콜의 타입 속성은 static 키워드를 작성하여 선언합니다.

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

 

하나의 인스턴스 프로퍼티를 가는 프로토콜의 예제입니다.

protocol FullyNamed {
    var fullName: String { get }
}

하나의 gettable 인스턴스 프로퍼티를 갖는 FullyNamed 프로토콜입니다.

 

아래는 FullyNamed 프로토콜을 따르는 하나의 구조체입니다.

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

Person 구조체의 각 인스턴스는 하나의 fullName 이라는 저장 프로퍼티를 가집니다.

이는 FullyNamed 프로토콜의 요구사항과 일치하고,

Person 구조체가 이 프로토콜을 따른다는 것을 의미합니다.

(만약 프로토콜의 요구사항을 만족하지 않으면 컴파일 에러가 발생합니다.)

 

다음은 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을 연산 프로퍼티로 선언하여

FullyNamed 프로토콜을 따릅니다.

 

Method Requirements

프로토콜은 이 프로토콜을 따르는 타입이 구현해야 하는

인스턴스 메서드와 타입 메서드를 요구할 수 있습니다.

이 메서드들은 메서드의 body와 괄호를 제외하고 나머지를 작성합니다.

가변 매개변수는 허용하지만 기본값은 설정할 수 없습니다.

 

타입 프로퍼티와 동일하게, static 키워드를 추가하여 타입 메서드를 선언할 수 있습니다.

protocol SomeProtocol {
    static func someTypeMethod()
}

 

아래 예제는 하나의 인스턴스 메서드 요구사항을 갖는 프로토콜을 정의합니다.

protocol RandomNumberGenerator {
    func random() -> Double
}

이 프로토콜을 따르는 타입은 random이라는 인스턴스 메서드를 가지고 있어야 하며,

이 메서드는 Double 값을 리턴합니다.

RandomNumberGenerator 프로토콜은 난수가 어떻게 생성되는지 지정하지는 않으며,

generator가 새로운 난수를 생성하는 표준을 제공하기만 합니다.

 

아래 예제는 RnadomNumberGenerator 프로토콜을 따르는 클래스의 정의를 보여줍니다.

이 클래스는 linear congruential generator로 알려진 난수 생성 알고리즘을 구현합니다.

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

self(인스턴스 자체)를 수정해야 하는 메서드가 필요할 때가 있습니다.

구조체나 열거형과 같은 값 타입에서의 인스턴스 메서드는 func 키워드 앞에 mutating 키워드를 작성하여

해당 메서드가 인스턴스 자체나 그 인스턴스의 프로퍼티를 수정할 수 있음을 나타냅니다.

 

프로토콜을 채택하는 모든 타입의 인스턴스를 변경하기 위한

프로토콜 인스턴스 메서드 요구사항을 정의하려면,

메서드에 mutating 키워드를 프로토콜 정의의 일부로 표시합니다.

이를 통해 구조체와 열거형에서 프로토콜을 채택하고, 해당 메서드 요구사항을 충족할 수 있습니다.

프로토콜에 mutating을 사용한 경우, 이 프로토콜을 따르는 클래스를 구현할 때는 mutating을 명시하지 않아도 됩니다.

 

아래에서 살펴볼 예제는 Togglable 프로토콜을 정의합니다.

이 프로토콜은 toggle이라는 하나의 인스턴스 메서드 요구사항을 정의합니다.

toggle()은 mutating 키워드가 추가되어 이를 따르는 인스턴스 상태를 변경할 수 있다는 것을 의미합니다.

protocol Togglable {
    mutating func toggle()
}

 

다음 코드는 OnOffSwitch 열거형을 정의합니다.

이 열거형은 on, off를 토글합니다.

값 타입이기 때문에 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

 

Initializer Requirements

프로토콜 요구사항에 지정된 이니셜라이저를 설정할 수 있습니다.

메서드와 마찬가지로 괄호와 이니셜라이저 body를 제외하고는

일반적인 이니셜라이저와 동일한 방법으로 작성하면 됩니다.

protocol SomeProtocol {
    init(someParameter: Int)
}

 

Class Implementations of Protocol Initializer Requirements

프로토콜을 따르는 클래스에

desinated 이니셜라이저 또는 convenience 이니셜라이저로 

프로토콜 이니셜라이저 요구사항을 구현할 수 있습니다.

이 경우에는 반드시 이니셜라이저 구현에 required 수식어를 붙여야 합니다.

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // initializer implementation goes here
    }
}

required 수식어를 사용하면 이를 따르는 클래스의 모든 서브클래스에

명시적/암시적인 이니셜라이저 요구사항 구현을 제공하도록 할 수 있습니다.

 

만약 서브클래스가 슈퍼클래스로부터 designated 이니셜라이저를

오버라이드하고 프로토콜로부터 일치하는 이니셜라이저 요구사항을 구현한다면,

이 이니셜라이저에는 required와 override를 둘 다 붙여야 합니다.

protocol SomeProtocol {
    init()
}
 
class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}
 
class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // initializer implementation goes here
    }
}

 

Failable Initializer Requirements

프로토콜에서 failable 이니셜라이저 요구사항을 정의할 수도 있습니다.

failable initalizer 요구사항은 이를 따르는 타입에서

failable 또는 non-failable 이니셜라이저에 의해 요구사항이 만족될 수 있습니다.

non-failable 이니셜라이저 요구사항은

non-failable 이니셜라이저 또는 implicitly unwrapped failable 이니셜라이저로 만족될 수 있습니다.

 

Protocols as Types

프로토콜은 실제로 기능 자체를 구현하지 않습니다.

그럼에도 프로토콜을 완전한 타입으로 사용할 수도 있습니다.

프로토콜을 타입으로 사용하는 것을 existential 타입이라고 하는데

이는 "there exists a type T such that T conforms to the protocol" 구절에서 유래되었습니다.

 

따라서 다음과 같이 다른 타입들이 허용되는 모든 곳에서 프로토콜을 사용할 수 있습니다.

  • 함수, 메서드, 이니셜라이저의 파라미터 타입이나 리턴 타입
  • 상수, 변수 또는 프로퍼티의 타입
  • 배열, 딕셔너리를 포함한 컨테이너 아이템 타입
프로토콜도 타입이므로 FullyNamed와 RandomNumberGenerator 처럼 
맨 앞 글자를 대문자로 작성합니다.

 

다음은 프로토콜을 타입으로 사용한 예제 코드입니다.

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

위 코드에서 Dice라는 클래스를 정의합니다.

 

상수 프로퍼티와 이니셜라이저의 파라미터에서

RandomNumberGenerator 프로토콜 자체를 타입으로 사용합니다.

generators는 RandomNumberGenerator 타입이기 때문에

random()를 구현하지 않아도 바로 접근할 수 있습니다.

 

아래 코드는 Dice 인스턴스 예제입니다.

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

 

Delegation

델리게이션(Delegation)은 클래스나 구조체가 책임(responsibilities)의 일부를 다른 타입의 인스턴스에

위임(hand off or delegate)할 수 있도록 하는 디자인 패턴입니다.

이 디자인 패턴은 위임된 책임을 캡슐화하는 프로토콜을 정의하여 구현되며,

이를 통해 이 프로토콜을 따르는 타입이 위임된 기능을 제공하도록 보장합니다.

 

아래 예제 코드는 주사위로 하는 보드 게임에서 사용하기 위한 두 가지 프로토콜을 정의합니다.

protocol DiceGame {
    var dice: Dice { get }
    func play()
}
protocol DiceGameDelegate: AnyObject {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

DiceGame 프로토콜은 주사위를 포함하는 어떠한 게임에서 적용될 수 있는 프로토콜입니다.

 

DiceGameDelegate는 DiceGame의 진행사항을 추적하기 위해 채택할 수 있습니다.

그리고 DiceGameDelegate를 AnyObject를 상속하여 클래스만 이 프로토콜을 따를 수 있도록 설정합니다.

 

아래 코드는 Snaked and Ladders 게임을 구현한 클래스입니다. (Control Flow 가이드에서 나온 그 게임)

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

SnakesAndLadders 클래스는 DiceGame 프로토콜을 따르며,

이 프로토콜은 gettable dice 프로퍼티와 play()를 제공합니다.

그리고 DiceGameDelegate 타입으로 delegate 변수를 weak 레퍼런스로 선언합니다.

 

delegate 프로퍼티는 옵셔널 DiceGameDelegate로 정의되어 있는데,

이는 게임을 진행하기 위한 필수사항은 아니기 때문입니다.

옵셔널 타입이기 때문에 자동으로 nil로 초기화됩니다.

추후 적절한 delegate를 설정할 수 있습니다.

DiceGameDelegate 프로토콜은 class-only이므로

순환 참조(reference cycle)를 방지하기 위해 weak로 delegate를 선언했습니다.

 

다음은 DiceGameTracker라는 클래스를 정의하는데,

이 클래스는 DiceGameDelegated 프로토콜을 따릅니다.

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

DiceGameTracker는 DiceGameDelegate 프로토콜에서 요구되는 3개의 메서드를 모두 구현합니다.

 

DiceGameTacker를 이용한 코드 예시입니다.

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

 

2편은 여기에서 볼 수 있습니다.

 

참고

https://docs.swift.org/swift-book/LanguageGuide/Protocols.html

 

 

반응형