프로토콜 (Protocols)

타입이 구현해야 하는 요구사항을 정의합니다.

*프로토콜(protocol)*은 특정 작업이나 기능에 적합한 메서드, 프로퍼티, 그리고 다른 요구사항의 청사진을 정의합니다. 프로토콜은 클래스, 구조체, 열거형이 채택하여 이 요구사항의 구현을 제공할 수 있습니다. 프로토콜의 요구사항을 만족하는 모든 타입은 프로토콜을 *준수(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
}

Note: 프로토콜은 타입이기 때문에 Swift에서 다른 타입의 이름처럼 (Int, String, Double 등) 대문자로 시작합니다 (FullyNamed, RandomNumberGenerator 등).

프로퍼티 요구사항 (Property Requirements)

프로토콜은 이를 준수하는 타입이 특정 이름과 타입을 가진 인스턴스 프로퍼티나 타입 프로퍼티를 제공하도록 요구할 수 있습니다. 프로토콜은 프로퍼티가 저장 프로퍼티나 연산 프로퍼티인지 지정하지 않습니다 --- 필요한 프로퍼티 이름과 타입만 지정합니다. 프로토콜은 각 프로퍼티가 읽기만 가능한지 읽기와 쓰기가 모두 가능한지도 지정합니다.

프로토콜이 프로퍼티의 읽기와 쓰기를 모두 요구하는 경우, 상수 저장 프로퍼티나 읽기 전용 계산 프로퍼티는 해당 요구사항을 충족할 수 없습니다. 프로토콜이 프로퍼티의 읽기만 요구하는 경우, 모든 종류의 프로퍼티로 요구사항을 충족할 수 있고 필요한 경우 쓰기가 가능하게 구현해도 무방합니다.

프로퍼티 요구사항은 항상 var 키워드로 변수 프로퍼티로 선언됩니다. 읽기와 쓰기 가능한 프로퍼티는 타입 선언 뒤에 { get set }으로 작성하여 나타내고, 읽기만 가능한 프로퍼티는 { get }으로 작성하여 나타냅니다.

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

프로토콜에 타입 프로퍼티 요구사항을 정의하려면 static 키워드를 붙여야 합니다. 클래스에 타입 프로퍼티 요구사항을 구현할 때는 classstatic 키워드를 사용할 수 있지만, 프로토콜에서는 static 키워드만 사용해야 합니다:

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

다음은 단일 인스턴스 프로퍼티 요구사항을 가지는 프로토콜 예시입니다:

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed 프로토콜은 이를 준수하는 타입이 완벽한 이름을 제공하도록 요구합니다. 이 프로토콜은 준수 타입의 성격에 대해 다른 것은 지정하지 않으며 --- 해당 타입이 전체 이름을 제공해야 된다고만 요구합니다. 이것은 FullyNamed 타입은 String 타입의 fullName이라는 읽기만 가능한 인스턴스 프로퍼티를 가져야 합니다.

다음은 FullyNamed 프로토콜을 채택하고 준수하는 구조체의 예시입니다:

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

이 예시는 특정 이름을 가진 사람을 나타내는 Person이라는 구조체를 정의합니다. 정의의 첫 번째 줄에 FullyNamed 프로토콜을 채택합니다.

Person의 각 인스턴스는 String 타입의 fullName이라는 단일 저장 프로퍼티를 가집니다. 이것은 FullyNamed 프로토콜의 단일 요구사항과 일치하고 Person은 프로토콜을 올바르게 준수하고 있다는 의미입니다. (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"

이 클래스는 스타십에 대해 읽기 전용 연산 프로퍼티로 fullName 프로퍼티 요구사항을 구현합니다. 각 Starship 클래스 인스턴스는 필수 name과 옵셔널 prefix를 저장합니다. fullName 프로퍼티는 prefix 값이 존재하면 사용하여 name의 앞에 추가해 스타십의 전체 이름을 생성합니다.

메서드 요구사항 (Method Requirements)

프로토콜은 이를 준수하는 타입이 특정 인스턴스 메서드와 타입 메서드를 구현하도록 요구할 수 있습니다. 이 메서드는 일반 인스턴스 메서드와 타입 메서드를 정의하는 방식과 동일하게 프로토콜의 정의에 작성되지만 중괄호나 메서드 본문 없이 작성합니다. 가변 매개변수(variadic parameter)도 일반 메서드와 같은 규칙에 따라 사용할 수 있습니다. 그러나 프로토콜의 정의 내에서 메서드 매개변수에 대해 기본값은 지정할 수 없습니다.

타입 프로퍼티 요구사항과 마찬가지로 프로토콜에 타입 메서드 요구사항을 정의할 때 static 키워드를 항상 표기합니다. 클래스에 타입 메서드 요구사항을 구현할 때는 classstatic 키워드를 사용할 수 있지만, 프로토콜에서는 static 키워드만 사용해야 합니다:

protocol SomeProtocol {
    static func someTypeMethod()
}

다음 예시는 단일 인스턴스 메서드 요구사항을 가지는 프로토콜을 정의합니다:

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator 프로토콜은 random 이라는 인스턴스 메서드 요구사항을 가지며 호출할 때마다 Double 값을 반환합니다. 프로토콜에 지정하지 않았지만, 이 값은 0.0부터 1.0미만의 숫자라고 가정합니다.

RandomNumberGenerator 프로토콜은 각 난수가 생성되는 방법에 대해 어떠한 것도 가정하지 않습니다 --- 이것은 새로운 난수를 생성하는 표준 방법을 제공할 것을 요구합니다.

다음은 RandomNumberGenerator 프로토콜을 채택하고 준수하는 클래스의 구현입니다. 이 클래스는 *선형 합동 생성기(linear congruential generator)*로 알려진 의사 난수(pseudorandom number) 생성 알고리즘을 구현합니다:

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 메서드 요구사항 (Mutating Method Requirements)

메서드가 속한 인스턴스를 수정이나 변경해야 하는 경우가 있습니다. 값 타입(구조체와 열거형)의 인스턴스 메서드의 경우 메서드의 func 키워드 앞에 mutating 키워드를 작성하여 메서드가 속한 인스턴스와 인스턴스의 모든 프로퍼티를 수정할 수 있음을 나타냅니다. 이 프로세스는 인스턴스 메서드 내부에서 값 타입 수정 (Modifying Value Types from Within Instance Methods)에 설명되어 있습니다.

프로토콜 인스턴스 메서드 요구사항이 프로토콜을 채택하는 모든 타입의 인스턴스도 변경할 수 있도록 정의하는 경우 프로토콜의 정의에서 해당 메서드에 mutating 키워드를 붙여야 합니다. 이를 통해 구조체와 열거형도 프로토콜을 채택하고 메서드 요구사항을 충족할 수 있습니다.

Note: mutating으로 프로토콜 인스턴스 메서드 요구사항을 표시하면, 클래스에 대한 해당 메서드의 구현을 작성할 때 mutating 키워드를 작성할 필요가 없습니다. mutating 키워드는 구조체와 열거형에서만 사용합니다.

아래의 예시는 toggle이라는 단일 인스턴스 메서드 요구사항을 정의하는 Togglable이라는 프로토콜을 정의합니다. 이름에서 알 수 있듯이, toggle() 메서드는 해당 타입의 프로퍼티를 수정하여 프로토콜을 준수하는 타입의 상태를 전환하거나 반전하기 위한 것입니다.

toggle() 메서드는 Togglable 프로토콜 정의에서 mutating 키워드로 표시되고, 이 메서드가 호출될 때 준수하는 인스턴스의 상태를 변경한다고 나타냅니다:

protocol Togglable {
    mutating func toggle()
}

구조체나 열거형에 Togglable 프로토콜을 구현하면, 해당 구조체나 열거형은 mutating으로 표시된 toggle() 메서드의 구현을 제공하여 프로토콜을 준수할 수 있습니다.

아래의 예시는 OnOffSwitch라는 열거형을 정의합니다. 이 열거형은 열거형 케이스 인 onoff를 나타내기 위해 두 가지 상태를 가집니다. 이 열거형의 toggle 구현은 Togglable 프로토콜의 요구사항에 맞게 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)

프로토콜은 이를 준수하는 타입이 특정 이니셜라이저를 구현하도록 요구할 수 있습니다. 이 이니셜라이저는 일반 이니셜라이저를 정의하는 방식과 동일하게 프로토콜의 정의에 작성되지만 중괄호나 이니셜라이저 본문 없이 작성합니다.

protocol SomeProtocol {
    init(someParameter: Int)
}

프로토콜 이니셜라이저 요구사항의 클래스 구현 (Class Implementations of Protocol Initializer Requirements)

프로토콜의 이니셜라이저 요구사항은 준수하는 클래스에서 지정 이니셜라이저나 편의 이니셜라이저로 구현할 수 있습니다. 이 경우 모두 required 수정자를 이니셜라이저 구현에 표시해야 합니다:

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

required 수정자를 사용하면 프로토콜을 준수하는 클래스의 모든 하위 클래스가 이니셜라이저 요구사항을 명시적으로 구현하거나 상속받도록 보장합니다.

더 자세한 정보는 필수 이니셜라이저 (Required Initializers)을 참고바랍니다.

Note: final 수정자로 표시한 클래스는 하위 클래스가 될 수 없으므로 프로토콜 이니셜라이저 구현에 required 수정자를 표시할 필요가 없습니다. final 수정자에 대한 자세한 내용은 재정의 방지 (Preventing Overrides)를 참고바랍니다.

하위 클래스가 상위 클래스의 지정 이니셜라이저를 재정의 하고 프로토콜의 동일한 이니셜라이저 요구사항도 구현해야 하면, requiredoverride 수정자 둘 다 이니셜라이저 구현에 표시해야 합니다:

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 Initializers)에 정의한대로 준수하는 타입에 대해 실패 가능한 이니셜라이저 요구사항을 정의할 수 있습니다.

실패 가능한 이니셜라이저 요구사항은 준수하는 타입에서 실패 가능한 이니셜라이저나 실패 불가능한 이니셜라이저로 충족될 수 있습니다. 실패 불가능한 이니셜라이저 요구사항은 실패 불가능한 이니셜라이저나 암시적 언래핑 된 실패 가능한 이니셜라이저로 충족될 수 있습니다.

의미론적 요구사항만 있는 프로토콜 (Protocols that Have Only Semantic Requirements)

위의 모든 프로토콜 예시는 일부 메서드나 프로퍼티를 요구하지만 프로토콜 선언에 요구사항을 포함하지 않아도 됩니다. 프로토콜을 사용해 의미론적(semantic) 요구사항을 기술할 수도 있습니다 --- 이것은 해당 타입의 값이 어떻게 동작하고 어떤 연산을 지원하는지에 대한 요구사항을 의미합니다.

Swift 표준 라이브러리는 여러 프로토콜을 정의하며 이것은 어떠한 메서드나 프로퍼티를 요구하지 않습니다:

이러한 프로토콜의 요구사항에 대한 자세한 내용은 이 문서의 개요를 참고바랍니다.

다른 프로토콜을 채택할 때와 동일한 문법을 사용하여 이러한 프로토콜을 채택합니다. 유일한 차이점은 프로토콜의 요구사항을 구현하는 메서드나 프로퍼티 선언이 포함되어 있지 않습니다. 예를 들어:

struct MyStruct: Copyable {
    var counter = 12
}

extension MyStruct: BitwiseCopyable { }

위 코드는 새로운 구조체를 정의합니다. Copyable은 의미론적 요구사항만 가지므로 구조체 선언에 프로토콜을 채택하기 위한 코드가 없습니다. 유사하게 BitwiseCopyable은 의미론적 요구사항만 가지므로 프로토콜을 채택하는 확장은 빈 본문을 가집니다.

보통 이러한 프로토콜의 준수를 작성할 필요가 없습니다 --- 대신 프로토콜 암시적 준수 (Implicit Conformance to a Protocol)에서 설명한대로 Swift는 암시적으로 준수를 추가합니다.

프로토콜을 타입으로 사용 (Protocols as Types)

프로토콜은 실제로 어떤 기능도 구현하지 않습니다. 그럼에도 불구하고 코드에서 프로토콜을 타입으로 사용할 수 있습니다.

프로토콜을 타입으로 사용하는 가장 일반적인 방법은 제네릭 제약조건(generic constraint)으로 프로토콜을 사용하는 것입니다. 제네릭 제약조건이 있는 코드는 프로토콜을 준수하는 어떠한 타입에서 동작할 수 있고 구체적인 타입은 API를 사용하는 코드에 의해 선택됩니다. 예를 들어 제네릭 타입의 인자를 받는 함수를 호출할 때, 호출자는 타입을 선택합니다.

불투명 타입(opaque type)을 사용하는 코드는 프로토콜을 준수하는 일부 타입에서 동작합니다. 실제 타입은 컴파일 시간에 알 수 있으며, API 구현에서 해당 타입을 선택하지만, 해당 타입의 식별자는 API의 클라이언트로 부터 숨겨집니다. 불투명 타입을 사용하면 API 의 자세한 구현이 추상 계층을 통해 노출되는 것을 방지할 수 있습니다 --- 예를 들어, 함수의 반환 타입을 숨기고 값이 특정 프로토콜을 준수한다는 것만 보장할 수 있습니다.

박싱 프로토콜 타입(boxed protocol type)을 사용하는 코드는 런타임 때 선택된 프로토콜을 준수하는 모든 타입에서 동작할 수 있습니다. 이런 런타임 유연성을 지원하기 위해, Swift는 필요할 때 성능 비용을 발생하는 *박스(box)*라는 간접 계층을 추가합니다. 이러한 유연성 때문에 Swift는 컴파일 시에 실제 타입을 알 수 없으므로, 프로토콜에 의해 요구되는 멤버만 접근할 수 있습니다. 실제 타입의 다른 API에 접근하려면 런타임에서 캐스팅이 필요합니다.

프로토콜을 제네릭 제약조건으로 사용하는 것에 대한 자세한 내용은 제네릭 (Generics)을 참고바랍니다. 불투명 타입과 박싱 프로토콜 타입에 대한 자세한 내용은 불투명 타입 (Opaque Types)을 참고바랍니다.

위임 (Delegation)

*위임(Delegation)*은 클래스나 구조체가 일부 책임을 다른 타입의 인스턴스에 넘길 수 있도록 하는 디자인 패턴입니다. 이 디자인 패턴은 위임할 책임을 캡슐화한 프로토콜을 정의해 구현하며, 이를 준수하는 타입(위임자)은 위임된 기능을 반드시 구현해야 합니다. 위임은 특정 동작에 응답하거나, 해당 소스의 구체적인 타입을 알 필요 없이 외부 소스에서 데이터를 가져올 때 사용할 수 있습니다.

아래 예시는 주사위 게임과 게임의 진행 사항을 관찰하는 위임에 대한 중첩된 프로토콜을 정의합니다:

class DiceGame {
    let sides: Int
    let generator = LinearCongruentialGenerator()
    weak var delegate: Delegate?

    init(sides: Int) {
        self.sides = sides
    }

    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }

    func play(rounds: Int) {
        delegate?.gameDidStart(self)
        for round in 1...rounds {
            let player1 = roll()
            let player2 = roll()
            if player1 == player2 {
                delegate?.game(self, didEndRound: round, winner: nil)
            } else if player1 > player2 {
                delegate?.game(self, didEndRound: round, winner: 1)
            } else {
                delegate?.game(self, didEndRound: round, winner: 2)
            }
        }
        delegate?.gameDidEnd(self)
    }

    protocol Delegate: AnyObject {
        func gameDidStart(_ game: DiceGame)
        func game(_ game: DiceGame, didEndRound round: Int, winner: Int?)
        func gameDidEnd(_ game: DiceGame)
    }
}

DiceGame 클래스는 각 플레이어가 주사위를 굴리고, 주사위를 굴려 가장 높은 숫자를 얻는 플레이어가 승리하는 게임을 구현합니다. 이전 챕터의 예시에서 사용한 선형 합동 생성기를 사용하여 주사위 굴림에 대한 난수를 생성합니다.

DiceGame.Delegate 프로토콜은 주사위 게임의 진행 사항을 추적하기 위해 채택할 수 있습니다. DiceGame.Delegate 프로토콜은 주사위 게임의 내에서 사용되기 때문에, DiceGame 클래스 내에 중첩되어 있습니다. 프로토콜은 외부 선언이 제네릭하지 않으면, 구조체와 클래스와 같은 타입 선언 내에 중첩될 수 있습니다. 중첩 타입에 대한 자세한 내용은 중첩 타입 (Nested Types)을 참고바랍니다.

강한 순환 참조를 방지하기 위해, 위임자(delegate)는 약한 참조로 선언됩니다. 약한 참조에 대한 자세한 내용은 클래스 인스턴스 간의 강한 순환 참조 (Strong Reference Cycles Between Class Instances)를 참고바랍니다. 프로토콜을 클래스 전용 프로토콜로 표시하면 DiceGame 클래스는 위임자가 약한 참조로 사용되어야 한다고 선언할 수 있습니다. 클래스 전용 프로토콜 (Class Only Protocols)에서 설명했듯이 클래스 전용 프로토콜은 AnyObject의 상속받음으로 표시합니다.

DiceGame.Delegate는 게임의 진행 사항을 추적하기 위해 세 개의 메서드를 제공합니다. 이 세 개의 메서드는 위의 play(rounds:) 메서드에서 게임 로직으로 사용됩니다. DiceGame 클래스는 새로운 게임이 시작하거나, 새로운 차례가 시작하거나, 게임이 끝날 때 해당 위임 메서드를 호출합니다.

delegate 프로퍼티는 옵셔널 DiceGame.Delegate이므로, play(rounds:) 메서드는 옵셔널 체이닝 (Optional Chaining)에서 설명한 대로 위임에 대한 메서드를 호출할 때마다 옵셔널 체이닝을 사용합니다. delegate 프로퍼티가 nil이면 이 위임자 호출은 무시됩니다. delegate 프로퍼티가 nil이 아니면 이 위임자 메서드가 호출되고, 매개변수로 DiceGame 인스턴스를 전달합니다.

다음 예시는 DiceGame.Delegate 프로토콜을 채택하는 DiceGameTracker라는 클래스는 나타냅니다:

class DiceGameTracker: DiceGame.Delegate {
    var playerScore1 = 0
    var playerScore2 = 0
    func gameDidStart(_ game: DiceGame) {
        print("Started a new game")
        playerScore1 = 0
        playerScore2 = 0
    }
    func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) {
        switch winner {
            case 1:
                playerScore1 += 1
                print("Player 1 won round \(round)")
            case 2: playerScore2 += 1
                print("Player 2 won round \(round)")
            default:
                print("The round was a draw")
        }
    }
    func gameDidEnd(_ game: DiceGame) {
        if playerScore1 == playerScore2 {
            print("The game ended in a draw.")
        } else if playerScore1 > playerScore2 {
            print("Player 1 won!")
        } else {
            print("Player 2 won!")
        }
    }
}

DiceGameTracker 클래스는 DiceGame.Delegate 프로토콜에 의해 요구되는 세 가지 메서드를 모두 구현합니다. 이 메서드는 새로운 게임이 시작되면 플레이어의 점수를 모두 0으로 초기화하고, 각 라운드가 끝날 때마다 점수를 업데이트하고, 게임이 끝나면 승자를 발표합니다.

DiceGameDiceGameTracker의 실제 동작은 다음과 같습니다:

let tracker = DiceGameTracker()
let game = DiceGame(sides: 6)
game.delegate = tracker
game.play(rounds: 3)
// Started a new game
// Player 2 won round 1
// Player 2 won round 2
// Player 1 won round 3
// Player 2 won!

확장으로 프로토콜 준수 추가 (Adding Protocol Conformance with an Extension)

기존 타입의 소스 코드에 접근할 수 없더라도 새로운 프로토콜을 채택하고 준수하도록 확장할 수 있습니다. 확장은 기존 타입에 새로운 프로퍼티, 메서드, 서브스크립트를 추가할 수 있으므로, 프로토콜이 요구하는 모든 요구사항을 추가할 수 있습니다. 자세한 내용은 확장 (Extensions)을 참고바랍니다.

Note: 확장을 통해 타입에 프로토콜 준수를 추가하면, 기존 인스턴스도 자동으로 해당 프로토콜을 채택하고 준수합니다.

예를 들어 TextRepresentable이라는 프로토콜은 텍스트로 표현할 수 있는 모든 타입이 구현할 수 있습니다. 이것은 자신에 대한 설명이거나 현재 상태의 텍스트 버전일 수 있습니다:

protocol TextRepresentable {
    var textualDescription: String { get }
}

위에서 Dice 클래스는 확장을 통해 TextRepresentable을 채택하고 준수할 수 있습니다:

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

이 확장은 Dice가 원래 구현에서 프로토콜을 채택한 것과 동일하게 새로운 프로토콜을 채택합니다. 프로토콜 이름은 콜론으로 구분된 타입 이름 뒤에 제공되고, 프로토콜의 모든 요구사항은 확장의 중괄호 내에 구현합니다.

이제 모든 Dice 인스턴스는 TextRepresentable로 취급될 수 있습니다:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"

마찬가지로 SnakesAndLadders 게임 클래스도 확장을 통해 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"

조건부 프로토콜 준수 (Conditionally Conforming to a Protocol)

제네릭 타입은 타입의 제네릭 매개변수가 프로토콜을 준수하는 경우와 같이 특정 조건에서만 프로토콜의 요구사항을 충족시킬 수 있습니다. 확장에서 제약조건을 명시하여 제네릭 타입이 조건부로 프로토콜을 준수하도록 만들 수 있습니다. 제네릭 where 절을 작성하여 채택할 프로토콜 이름 뒤에 제약조건을 명시합니다. 제네릭 where 절에 대한 자세한 내용은 제네릭 Where 절 (Generic Where Clauses)을 참고바랍니다.

다음 확장은 Array 인스턴스가 저장하는 항목이 TextRepresentable을 준수할 때에만 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]"

확장으로 프로토콜 채택 선언 (Declaring Protocol Adoption with an Extension)

타입이 이미 프로토콜의 모든 요구사항을 준수하지만, 해당 프로토콜을 채택한다고 아직 명시하지 않은 경우, 빈 확장을 사용하여 프로토콜을 채택하도록 만들 수 있습니다:

struct Hamster {
    var name: String
    var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

Hamster의 인스턴스는 TextRepresentable이 요구되는 타입 어디에서나 사용할 수 있습니다:

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"

Note: 타입은 요구사항이 충족된다고 해서 프로토콜을 자동으로 채택하지 않습니다. 항상 프로토콜 채택을 명시적으로 선언해야 합니다.

자동 생성 구현을 사용하여 프로토콜 채택 (Adopting a Protocol Using a Synthesized Implementation)

Swift는 많은 경우에 Equatable, Hashable, Comparable에 대해 프로토콜 준수성을 자동으로 제공할 수 있습니다. 이렇게 합성 구현을 사용하면 프로토콜 요구사항 구현을 위해 반복적인 상용구 코드를 작성할 필요가 없습니다.

Swift는 다음과 같은 커스텀 타입에 대해 Equatable의 합성 구현을 제공합니다:

  • Equatable 프로토콜을 준수하는 저장 프로퍼티만 있는 구조체

  • Equatable 프로토콜을 준수하는 연관 타입만 있는 열거형

  • 연관 타입이 없는 열거형

==의 합성 구현을 받기 위해선, == 연산자를 직접 구현하지 않고 원래 선언을 포함한 파일에서 Equatable을 채택한다고 선언합니다. Equatable 프로토콜은 !=의 기본 구현도 제공합니다.

아래의 예시는 Vector2D 구조체와 유사한 3차원의 벡터 (x, y, z)에 대한 Vector3D 구조체를 정의합니다. x, y, z 프로퍼티는 모두 Equatable 타입이므로, Vector3D는 등가 연산자의 합성 구현을 받습니다.

struct Vector3D: Equatable {
    var x = 0.0, y = 0.0, z = 0.0
}

let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
if twoThreeFour == anotherTwoThreeFour {
    print("These two vectors are also equivalent.")
}
// Prints "These two vectors are also equivalent."

Swift는 다음과 같은 커스텀 타입에 대해 Hashable의 합성 구현을 제공합니다:

  • Hashable 프로토콜을 준수하는 저장 프로퍼티만 가지는 구조체

  • Hashable 프로토콜을 준수하는 연관 타입만 가지는 열거형

  • 연관 타입이 없는 열거형

hash(into:)의 합성 구현을 받기 위해선, hash(into:) 메서드를 직접 구현하지 않고 원래 선언을 포함한 파일에서 Hashable을 채택한다고 선언합니다.

Swift는 원시값이 없는 열거형에 대해 Comparable의 합성 구현을 제공합니다. 열거형이 연관 타입을 가지고 있다면, 모두 Comparable 프로토콜을 준수해야 합니다. <의 합성 구현을 받기 위해선, < 연산자를 직접 구현하지 않고 원래 열거형 선언을 포함한 파일에서 Comparable을 채택한다고 선언합니다. <=, >, >=Comparable 프로토콜의 기본 구현도 자동으로 제공합니다.

아래의 예시는 초보자, 중급자, 전문가의 케이스를 가진 SkillLevel 열거형을 정의합니다. 전문가는 가진 별의 숫자에 따라 추가적으로 순위가 매겨집니다.

enum SkillLevel: Comparable {
    case beginner
    case intermediate
    case expert(stars: Int)
}
var levels = [SkillLevel.intermediate, SkillLevel.beginner,
              SkillLevel.expert(stars: 5), SkillLevel.expert(stars: 3)]
for level in levels.sorted() {
    print(level)
}
// Prints "beginner"
// Prints "intermediate"
// Prints "expert(stars: 3)"
// Prints "expert(stars: 5)"

프로토콜 암시적 준수 (Implicit Conformance to a Protocol)

일부 프로토콜은 너무 흔해서 새로운 타입을 선언할 때 거의 항상 작성하게 됩니다. 다음의 프로토콜의 경우 프로토콜의 요구사항을 구현하는 타입을 정의할 때 Swift가 자동으로 준수를 추론하므로 직접 작성할 필요가 없습니다:

명시적으로 준수를 작성할 수 있지만 코드의 동작을 변경하지 않습니다. 암시적 준수를 억제하려면 준수 목록에서 프로토콜 이름 앞에 물결표(~)를 작성합니다:

struct FileDescriptor: ~Sendable {
    let rawValue: Int
}

위 코드는 POSIX 파일 디스크립터를 감싸는 래퍼의 일부를 보여줍니다. FileDescriptor 구조체는 Sendable 프로토콜의 요구사항을 모두 충족하여 일반적으로 Sendable합니다. 그러나 ~Sendable 작성하는 것은 암시적 준수를 억제합니다. 파일 디스크립터는 열린 파일을 식별하고 상호작용하기 위해 정수를 사용하고 정수값은 Sendable함에도 불구하고 이를 Sendable하지 않게 만드는 것은 특정 버그를 피하는데 도움을 줄 수 있습니다.

암시적 준수를 억제하는 다른 방법은 비가용성으로 표시된 확장을 사용하는 것입니다:

@available(*, unavailable)
extension FileDescriptor: Sendable { }

이전 예시처럼 코드의 한 곳에 ~Sendable을 작성하면, 프로그램의 다른 곳에 있는 코드가 FileDescriptor 타입에 Sendable 준수를 추가하여 확장할 수 있습니다. 반면에 이 예시의 비가용성 확장은 Sendable의 암시적 준수를 억제할 뿐만 아니라 코드의 다른 곳에 있는 어떤 확장도 해당 타입에 Sendable 준수를 추가하는 것을 방지합니다.

Note: 위에 언급한 프로토콜 외에도 분산 액터(distributed actors)는 Codable 프로토콜을 암시적으로 준수합니다.

프로토콜 타입의 컬렉션 (Collections of Protocol Types)

프로토콜은 프로토콜을 타입으로 사용 (Protocols as Types)에서 언급했듯이 배열이나 딕셔너리와 같은 컬렉션의 타입으로 사용할 수 있습니다. 이 예시는 TextRepresentable에 대한 배열을 생성합니다:

let things: [TextRepresentable] = [game, d12, simonTheHamster]

이제 배열에 항목을 반복할 수 있고, 각 항목의 설명을 출력할 수 있습니다:

for thing in things {
    print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

thing 상수는 TextRepresentable 타입입니다. 실제 인스턴스가 Dice, DiceGame, Hamster 중 하나 이지만 이 타입은 아닙니다. 그럼에도 불구하고 TextRepresentable 타입이므로, TextRepresentabletextualDescription 프로퍼티를 가지고 있다는 것을 알고 있으므로 루프를 통해 thing.textualDescription에 안전하게 접근할 수 있습니다.

프로토콜 상속 (Protocol Inheritance)

프로토콜은 하나 이상의 다른 프로토콜을 상속할 수 있고, 상속받은 요구사항에 새로운 요구사항을 추가할 수 있습니다. 프로토콜 상속의 문법은 클래스 상속 문법과 유사하지만, 여러 프로토콜을 콤마로 구분하여 나열할 수 있습니다:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}

다음은 위의 TextRepresentable 프로토콜을 상속하는 프로토콜의 예입니다:

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

이 예시는 TextRepresentable을 상속하는 PrettyTextRepresentable이라는 새로운 프로토콜을 정의합니다. PrettyTextRepresentable을 채택하는 타입은 TextRepresentable의 모든 요구사항을 만족해야 하며, 추가PrettyTextRepresentable의 모든 요구사항도 만족해야 합니다. 이 예시에서 PrettyTextRepresentableString을 반환하는 prettyTextualDescription이라는 읽기 전용 프로퍼티 요구사항을 추가합니다.

SnakesAndLadders 클래스는 확장을 통해 PrettyTextRepresentable을 채택하고 준수할 수 있습니다:

extension SnakesAndLadders: PrettyTextRepresentable {
    var prettyTextualDescription: String {
        var output = textualDescription + ":\n"
        for index in 1...finalSquare {
            switch board[index] {
            case let ladder where ladder > 0:
                output += "▲ "
            case let snake where snake < 0:
                output += "▼ "
            default:
                output += "○ "
            }
        }
        return output
    }
}

이 확장은 PrettyTextRepresentable 프로토콜을 채택하고 SnakesAndLadders 타입에 대해 prettyTextualDescription 프로퍼티의 구현을 제공한다고 나타냅니다. PrettyTextRepresentable을 채택한 타입은 TextRepresentable도 채택해야 하므로, prettyTextualDescription의 구현은 출력 문자열을 위해 TextRepresentable 프로토콜에서 textualDescription 프로퍼티를 접근하는 것으로 시작합니다. 콜론과 줄 바꿈을 추가하고 정리된 텍스트 표현을 시작으로 사용합니다. 그런 다음 보드 사각형의 배열을 통해 반복하고, 각 사각형의 내용을 나타내기 위해 기호를 추가합니다:

  • 사각형의 값이 0보다 크면, 사다리의 밑부분이 되고 로 표시합니다.

  • 사각형의 값이 0보다 작으면, 뱀의 머리이고 으로 표시합니다.

  • 사각형의 값이 0이고 "자유" 정사각형이면, 으로 표시합니다.

prettyTextualDescription 프로퍼티는 이제 모든 SnakesAndLadders 인스턴스의 텍스트 설명을 출력하기 위해 사용할 수 있습니다:

print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

클래스 전용 프로토콜 (Class-Only Protocols)

프로토콜 상속 목록에 AnyObject 프로토콜을 추가여 해당 프로토콜을 클래스 타입(구조체나 열거형이 아닌)에서만 채택할 수 있게 제한할 수 있습니다.

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // class-only protocol definition goes here
}

위의 예시에서 SomeClassOnlyProtocol은 클래스 타입서에만 채택할 수 있습니다. SomeClassOnlyProtocol을 구조체나 열거형에 채택하면 컴파일 오류가 발생합니다.

Note: 프로토콜 요구사항이 참조 타입의 특성을 필요로 하거나 기대하는 경우 클래스 전용 프로토콜을 사용합니다. 참조 타입과 값 타입에 대한 자세한 내용은 구조체와 열거형은 값 타입 (Structures and Enumerations Are Value Types)클래스는 참조 타입 (Classes Are Reference Types)을 참고바랍니다.

프로토콜 합성 (Protocol Composition)

동시에 여러 프로토콜을 준수하는 타입을 요구하는 것이 유용할 수 있습니다. *프로토콜 합성(protocol composition)*을 사용하여 여러 프로토콜을 단일 요구사항으로 결합할 수 있습니다. 프로토콜 합성은 모든 프로토콜의 요구사항을 가진 임시 로컬 프로토콜을 정의한 것처럼 동작합니다. 프로토콜 합성은 새로운 프로토콜 타입을 정의하지 않습니다.

프로토콜 합성은 SomeProtocol & AnotherProtocol 형태입니다. 앰퍼샌드(&)로 구분하여 많은 프로토콜을 나열할 수 있습니다. 프로토콜 목록 외에도 프로토콜 합성은 클래스 타입 하나를 포함시켜 필수 상위 클래스를 지정할 수도 있습니다.

다음은 NamedAged라는 두 프로토콜을 함수 매개변수에 단일 요구사항으로 결합한 예입니다:

protocol Named {
    var name: String { get }
}
protocol Aged {
    var age: Int { get }
}
struct Person: Named, Aged {
    var name: String
    var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"

이 예시에서 Named 프로토콜은 name이라는 읽기 전용 String 프로퍼티인 단일 요구사항을 가지고 있습니다. Aged 프로토콜은 age라는 읽기 전용 Int 프로퍼티인 단일 요구사항을 가지고 있습니다. 두 프로토콜 모두 Person 구조체에서 채택합니다.

예시는 wishHappyBirthday(to:) 함수도 정의합니다. celebrator 매개변수의 타입은 "NamedAged 프로토콜 모두 준수하는 타입"이라는 의미인 Named & Aged입니다. 요구된 프로토콜 모두 준수하는 한 함수에 전달하는 특정 타입은 중요하지 않습니다.

그런 다음 birthdayPerson이라는 새로운 Person 인스턴스를 생성하고, wishHappyBirthday(to:) 함수에 새로운 인스턴스를 전달합니다. Person은 프로토콜을 모두 준수하기 때문에 이 호출은 유효하고 wishHappyBirthday(to:) 함수는 생일 메세지를 출력할 수 있습니다.

다음은 Location 클래스와 이전 예시에서의 Named 프로토콜을 결합한 예입니다:

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"

beginConcert(in:) 함수는 "Location의 하위 클래스이면서 Named 프로토콜을 준수하는 모든 타입"이라는 의미의 Location & Named 타입의 매개변수를 가집니다. 이 경우 City는 이 요구사항에 충족합니다.

PersonLocation의 하위 클래스가 아니므로, beginConcert(in:) 함수로 birthdayPerson 전달은 유효하지 않습니다. 마찬가지로 Named 프로토콜을 준수하지 않고 Location의 하위 클래스를 만들어 타입의 인스턴스로 beginConcert(in:)을 호출하는 것도 유효하지 않습니다.

프로토콜 준수 여부 검사 (Checking for Protocol Conformance)

프로토콜 준수 여부를 확인하거나 특정 프로토콜로 캐스팅 하기 위해 타입 캐스팅 (Type Casting)에서 설명한 isas 연산자를 사용할 수 있습니다. 프로토콜을 확인하고 캐스팅하는 것은 타입을 확인하고 캐스팅하는 것과 정확하게 같은 문법을 따릅니다:

  • is 연산자는 인스턴스가 프로토콜을 준수한다면 true를 반환하고, 그렇지 않으면 false를 반환합니다.

  • 다운캐스트 연산자의 as? 버전은 프로토콜의 타입의 옵셔널 값을 반환하고 인스턴스가 프로토콜을 준수하지 않으면 nil을 반환합니다.

  • 다운캐스트 연산자의 as! 버전은 프로토콜 타입으로 강제로 다운캐스트하고 다운캐스트가 성공하지 못하면 런타임 오류가 발생합니다.

아래 예시는 area라는 읽기 전용 Double 프로퍼티의 단일 프로퍼티 요구사항을 가지는 HasArea라는 프로토콜을 정의합니다:

protocol HasArea {
    var area: Double { get }
}

다음은 HasArea 프로토콜을 준수하는 CircleCountry인 두 개의 클래스 입니다:

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }
    init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}

Circle 클래스는 저장 프로퍼티 radius를 기반으로 연산 프로퍼티 area를 구현합니다. Country 클래스는 저장 프로퍼티로 직접 area 요구사항을 구현합니다. 두 클래스 모두 HasArea 프로토콜을 준수합니다.

다음은 HasArea 프로토콜을 준수하지 않는 Animal이라는 클래스 입니다:

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

Circle, Country, Animal 클래스는 공통 상위 클래스가 없습니다. 그럼에도 불구하고 모두 클래스이므로 이 세 가지 타입의 인스턴스는 타입 AnyObject의 값을 저장하는 배열에 저장할 수 있습니다:

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

objects 배열은 2의 반지름을 가진 Circle 인스턴스, 제곱 킬로미터 단위로 영국의 면적으로 초기화 된 Country 인스턴스, 네 개의 다리를 가진 Animal 인스턴스를 포함하는 배열 리터럴로 초기화 됩니다.

objects 배열은 이제 반복될 수 있고, 배열의 각 객체는 HasArea 프로토콜을 준수하는지 확인할 수 있습니다:

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

배열의 객체가 HasArea 프로토콜을 준수하면, as? 연산자가 반환하는 옵셔널 값은 objectWithArea라는 상수에 옵셔널 바인딩으로 언래핑합니다. objectWithArea 상수는 HasArea 타입으로 알고 있으므로, area 프로퍼티는 접근 가능하고 안전하게 출력할 수 있습니다.

기본 객체는 캐스팅 과정에 의해 변경되지 않습니다. 그것은 계속해서 Circle, Country, Animal입니다. 그러나 objectWithArea 상수에 저장될 때, HasArea 타입으로만 알고 있으므로, area 프로퍼티만 접근 가능합니다.

옵셔널 프로토콜 요구사항 (Optional Protocol Requirements)

프로토콜에 *옵셔널 요구사항(optional requirements)*을 정의할 수 있습니다. 이 요구사항은 프로토콜을 준수하는 타입이 구현할 필요가 없습니다. 옵셔널 요구사항은 프로토콜의 정의에 optional 수정자를 앞에 붙입니다. 옵셔널 요구사항은 Objective-C와 상호 운용되는 코드를 작성할 수 있습니다. 프로토콜과 옵셔널 요구사항 모두 @objc 속성으로 표시되어야 합니다. @objc 프로토콜은 구조체나 열거형에서 채택할 수 없고 클래스에만 채택할 수 있습니다.

옵셔널 요구사항의 메서드나 프로퍼티를 사용할 때, 그 타입은 자동으로 옵셔널이 됩니다. 예를 들어 (Int) -> String 타입의 메서드는 ((Int) -> String)?이 됩니다. 전체 함수 타입은 메서드의 반환값이 아니라 옵셔널로 래핑됩니다.

옵셔널 프로토콜 요구사항은 프로토콜을 준수하는 타입에 요구사항이 구현되어 있지 않을 수 있으므로, 옵셔널 체이닝으로 호출할 수 있습니다. 호출할 때 someOptionalMethod?(someArgument)와 같이 메서드의 이름 뒤에 물음표를 작성하여 옵셔널 메서드를 호출합니다. 옵셔널 체이닝에 대한 자세한 내용은 옵셔널 체이닝 (Optional Chaining)에서 확인할 수 있습니다.

다음 예시는 증가하는 값을 제공하기 위해 외부 데이터 소스를 사용하는 Counter라는 정수 카운팅 클래스를 정의합니다. 이 데이터 소스는 두 개의 옵셔널 요구사항을 가진 CounterDataSource 프로토콜로 정의됩니다:

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}

CounterDataSource 프로토콜은 increment(forCount:)라는 옵셔널 메서드 요구사항과 fixedIncrement라는 옵셔널 프로퍼티 요구사항을 정의합니다. 이 요구사항은 Counter 인스턴스에 대해 적절한 증가 값을 제공하기 위한 두 가지 방법을 정의합니다.

Note: 엄밀히 말하면 프로토콜 요구사항을 구현하지 않고도 CounterDataSource를 준수하는 커스텀 클래스를 작성할 수 있습니다. 둘 다 옵셔널 입니다. 기술적으로는 가능하지만 좋은 데이터 소스로는 적합합지 않습니다.

아래 정의된 Counter 클래스는 CounterDataSource? 타입의 옵셔널 dataSource 프로퍼티를 가지고 있습니다:

class Counter {
    var count = 0
    var dataSource: CounterDataSource?
    func increment() {
        if let amount = dataSource?.increment?(forCount: count) {
            count += amount
        } else if let amount = dataSource?.fixedIncrement {
            count += amount
        }
    }
}

Counter 클래스는 count라는 변수 프로퍼티에 현재값을 저장합니다. Counter 클래스는 메서드가 호출될 때마다 count 프로퍼티를 증가시키는 increment라는 메서드도 정의합니다.

increment() 메서드는 먼저 데이터 소스의 increment(forCount:) 메서드를 통해 증가 값을 조회하려고 합니다. increment() 메서드는 increment(forCount:) 호출에 대해 옵셔널 체이닝을 사용하고 메서드의 단일 인자로 현재 count 값을 전달합니다.

여기서 두 단계 옵셔널 체이닝을 사용합니다. 먼저 dataSourcenil일 수 있으므로, dataSourcenil이 아닌 경우에만 increment(forCount:) 호출해야 된다는 것을 나타내기 위해 dataSource 이름 뒤에 물음표를 붙입니다. 두 번째로 옵셔널 요구사항 이므로, dataSource가 존재하더라도 increment(forCount:)가 구현되어 있다고 보장하지 않습니다. 여기서 increment(forCount:)가 구현되어 있지 않다는 가능성을 옵셔널 체이닝으로 처리합니다. increment(forCount:)가 존재할 경우에만 increment(forCount:) 호출이 이뤄지고 존재하지 않으면 nil입니다. 이것이 increment(forCount:) 이름 뒤에 물음표가 붙는 이유입니다.

increment(forCount:) 호출은 위의 2가지 이유로 실패할 수 있으므로, 이 호출은 항상 옵셔널 Int 값을 반환합니다. increment(forCount:)CounterDataSource의 정의에서 옵셔널이 아닌 Int 값으로 반환하더라도 마찬가지입니다. 두 개의 옵셔널 체이닝 연산자가 차례로 있지만 결과는 여전히 단일 옵셔널로 래핑됩니다. 여러 개 옵셔널 체이닝 연산자 사용에 대한 자세한 내용은 여러 단계의 체이닝 연결 (Linking Multiple Levels of Chaining)을 참고바랍니다.

increment(forCount:) 호출 후에 반환한 옵셔널 Int는 옵셔널 바인딩을 사용하여 amount라는 상수에 언래핑됩니다. 옵셔널 Int에 값이 포함되어 있다면 —-- 이것은 위임자와 메서드가 모두 존재하고 메서드가 값을 반환 한 경우 —-- 언래핑된 amountcount 저장 프로퍼티에 추가하고 증가를 완료합니다.

dataSourcenil이거나 데이터 소스가 increment(forCount:)를 구현하지 않아 increment(forCount:) 메서드로 부터 값을 조회할 수 없는 경우에는 increment() 메서드는 데이터 소스의 fixedIncrement 프로퍼티를 대신 조회하려고 합니다. fixedIncrement 프로퍼티도 옵셔널 요구사항이므로, 그 값은 fixedIncrementCounterDataSource 프로토콜 정의에 옵셔널이 아닌 Int 프로퍼티로 정의되었어도 옵셔널 Int 값입니다.

다음은 데이터 소스를 매번 조회할 때마다 3의 상수 값을 반환하는 간단한 CounterDataSource 구현 입니다. 옵셔널 fixedIncrement 프로퍼티 요구사항을 구현하여 이것을 수행합니다:

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}

새로운 Counter 인스턴스의 데이터 소스로 ThreeSource 인스턴스를 사용할 수 있습니다:

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
// 3
// 6
// 9
// 12

위의 코드는 새로운 Counter 인스턴스를 생성하고 이 데이터 소스를 새로운 ThreeSource 인스턴스로 설정하고 카운터의 increment() 메서드를 네 번 호출합니다. 예상대로 카운터의 count 프로퍼티는 increment()가 호출될 때마다 3씩 증가합니다.

다음은 Counter 인스턴스를 현재 count 값에서 0으로 증가나 감소시키는 TowardsZeroSource라는 더 복잡한 데이터 소스입니다:

class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}

TowardsZeroSource 클래스는 CounterDataSource 프로토콜의 옵셔널 increment(forCount:) 메서드를 구현하고 카운트 방향을 정하기 위해 count 인자값을 사용합니다. count가 0이면 이 메서드는 더이상 카운트 작업을 진행하지 않음을 나타내기 위해 0을 반환합니다.

-4부터 0까지 카운트 하기 위해 기존 Counter 인스턴스에 TowardsZeroSource의 인스턴스를 사용할 수 있습니다. 카운터가 0에 도달하면 더이상 카운팅이 동작하지 않습니다:

counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0

프로토콜 확장 (Protocol Extensions)

프로토콜은 확장을 통해 메서드, 이니셜라이저, 서브스크립트, 연산 프로퍼티 구현을 준수 타입에 제공할 수 있습니다. 이를 통해 각 타입의 개별 구현이나 전역 함수가 아닌 프로토콜 자체에 동작을 정의할 수 있습니다.

예를 들어 RandomNumberGenerator 프로토콜은 확장을 통해 임의의 Bool 값을 반환하기 위해 필수 random() 메서드의 결과를 사용하는 randomBool() 메서드를 제공할 수 있습니다:

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

프로토콜 확장을 생성함으로써 모든 준수 타입은 추가 수정없이 이 메서드를 사용할 수 있습니다.

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// Prints "And here's a random Boolean: true"

프로토콜 확장은 준수 타입에 구현을 추가할 수 있지만 다른 프로토콜을 확장하거나 다른 프로토콜을 상속할 수 없습니다. 프로토콜 상속은 항상 프로토콜 선언 자체에서만 지정할 수 있습니다.

기본 구현 제공 (Providing Default Implementations)

프로토콜의 메서드나 연산 프로퍼티 요구사항에 기본 구현을 제공하기 위해 프로토콜 확장을 사용할 수 있습니다. 준수 타입이 필수 메서드나 프로퍼티의 자체 구현을 제공하면, 그 구현은 확장에서 제공하는 기본 구현 대신 사용됩니다.

Note: 확장으로 제공된 기본 구현을 가진 프로토콜 요구사항은 옵셔널 프로토콜 요구사항과 다릅니다. 준수 타입이 자체 구현을 제공하지 않아도 되지만, 기본 구현을 가지므로 옵셔널 체이닝 없이 호출할 수 있습니다.

예를 들어 TextRepresentable 프로토콜을 상속하는 PrettyTextRepresentable 프로토콜은 textualDescription 프로퍼티 접근의 결과를 반환하기 위해 필요한 prettyTextualDescription 프로퍼티의 기본 구현을 제공할 수 있습니다:

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return textualDescription
    }
}

프로토콜 확장에 제약조건 추가 (Adding Constraints to Protocol Extensions)

프로토콜 확장을 정의할 때, 확장의 메서드와 프로퍼티를 사용할 수 있기 전에 준수 타입이 만족해야 하는 제약조건을 지정할 수 있습니다. 제네릭 where 절을 작성하여 확장하는 프로토콜의 이름 뒤에 제약조건을 작성합니다. 제네릭 where 절의 자세한 내용은 제네릭 Where 절 (Generic Where Clauses)을 참고바랍니다.

예를 들어 Equatable 프로토콜을 준수하는 항목의 모든 컬렉션에 적용하는 Collection 프로토콜의 확장을 정의할 수 있습니다. 컬렉션의 요소를 Swift 표준 라이브러리의 Equatable 프로토콜로 제한하면 두 요소 간의 같음과 다름에 대한 확인을 위해 ==!= 연산자를 사용할 수 있습니다.

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}

allEqual() 메서드는 컬렉션에 모든 요소가 같을 때만 true를 반환합니다.

모든 요소가 같고 하나만 다른 정수의 두 배열을 생각해 봅시다:

let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]

배열은 Collection을 준수하고 정수는 Equatable을 준수하므로, equalNumbersdifferentNumbersallEqual() 메서드를 사용할 수 있습니다:

print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

Note: 준수 타입이 동일한 메서드나 프로퍼티에 대한 구현을 제공하는 여러 제약조건의 확장에 대한 요구사항을 충족한다면 Swift는 가장 특화된 제약조건의 구현을 사용합니다.

Last updated

Was this helpful?