Swift 뿌수기 - Protocols

Wonbi·2022년 9월 19일
0

Swift 뿌수기

목록 보기
9/12

✅ 학습 내용

💎 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 those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol.

  • 프로토콜 (protocol) 은 특별한 임무 또는 기능 조각에 적합한 메소드와, 속성, 및 다른 필수 조건들의 청사진을 정의합니다. 그러면 클래스나, 구조체, 또는 열거체가 프로토콜을 채택 (adopt) 하여 그 필수 조건들의 실제 구현을 제공할 수 있습니다. 프로토콜의 필수 조건을 만족하는 어떤 타입이든 그 프로토콜을 준수한다 (conform) 고 말합니다.

  • 준수 타입 (conforming types) 이 반드시 구현해야 하는 요구 사항을 지정하는 것 외에도, 프로토콜을 확장하면 준수 타입이 이러한 필수 조건의 일부 또는 추가 기능의 구현의 이점을 취할 수 있다.

✏️ 프로토콜 구문

  • 프로토콜은 클래스, 구조체, 및 열거형과 아주 비슷한 방식으로 정의한다.
protocol SomeProtocol {
  // 프로토콜 정의를 여기에 쓴다.
}
  • 사용자 정의 타입은 타입 이름 뒤에 프로토콜 이름을 콜론으로 구분함으로써 특정한 프로토콜을 채택한다고 알린다. 여러개의 프로토콜을 쉼표로 구분하여 나열할 수도 있다.
struct SomeStructure: FirstProtocol, AnotherProtocol {
  // 구조체 정의는 여기에 쓴다.
}
  • 클래스의 상위 클래스가 있다면, 채택한 프로토콜들 보다 앞에 상위 클래스 이름을 나열한 후 프로토콜을 나열한다.
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
  // 클래스 정의는 여기에 쓴다.
}

✏️ 프로퍼티 필수 조건

  • 프로토콜은 어떤 준수 타입이든 특별한 이름과 타입의 인스터스 프로퍼티 및 타입 프로퍼티를 제공하도록 요구할 수 있다. 프로토콜은 프로퍼티가 저장 프로퍼티인지 연산 프로퍼티인지는 지정하지 않는다. 필수인 프로퍼티 이름과 타입만 지정한다. 프로토콜은 각각의 프로퍼티가 반드시 획득 가능해야 하는지(get), 아니면 획득 가능 하면서, 설정도 가능해야 하는지(get, set)도 지정한다.

  • 프로토콜이 획득 가능하면서 설정 가능한 속성을 요구하는 경우, 상수 저장 프로퍼티나 읽기 전용 연산 프로퍼티론 그 프로퍼티의 필수 조건을 충족할 수 없다. 프로토콜이 획득 가능한 속상만 요구하는 경우, 어떤 종류의 프로퍼티도 필수 조건을 만족할 수 있으며, 프로퍼티를 설정도 가능하게 하는게 자신의 코드에 유용하면 이러는 것도 유효하다.

  • 프로퍼티의 필수 조건은 항상 var 키워드 접두사를 가진 변수로 선언한다. 획득 가능하면서 설정 가능한 프로퍼티는 자신의 타입 선언 뒤에 { get set } 을 써서 지시하며, 획득 가능한 속성은 { get } 을 써서 지시한다.

protocol SomeProtocol {
  var mustBeSettable: Int { get set }
  var doesNotNeedToBeSettable: Int { get }
}
  • 타입 프로퍼티 필수 조건은 정의할 때 항상 static 키워드 접두사를 붙인다. 이 규칙은 클래스가 구현할 때 타입 프로퍼티 필수 조건에 classstatic 키워드 접두사를 붙일지라도 그대로 적용된다.
protocol AnotherProtocol {
  static var someTypeProperty: Int { get set }
}
  • 단일 인스턴스 프로퍼티 필수 조건을 가진 프로토콜은 다음과 같다.
protocol FullyNamed {
  var fullName: String { get }
}
  • FullyNamed 프로토콜을 채택하고 준수하는 단순한 구조체 예제는 다음과 같다.
struct Person: FullyNamed {
  var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName 은 "John Appleseed" 임
  • 좀 더 복잡한 예제는 다음과 같다.
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 은 "USS Enterprise" 임

✏️ 메서드 필수 조건

  • 프로토콜은 준수 타입이 특정 인스턴스 메서드 및 타입 메서드를 구현하도록 요구할 수 있다. 이 메서드들은 보통의 인스턴스 및 타입 메서드와 정확히 똑같은 방식으로 프로토콜 정의 부분에 구현하지만, 중괄호나 메서드 로직을 작성하는 본문이 없다. 보통의 메서드에서와 규칙이 동일하다는 전제로, 가변 매개변수를 허용한다. 하지만, 프로토콜 정의 안에 메서드 매개변수에서 기본 값을 지정할 수는 없다.

  • 타입 프로퍼티 필수 조건처럼, 프로토콜에서 타입 메서드 필수 조건을 정의할 땐 항상 static 키워드 접두사를 붙인다. 타입 메서드 필수 조건을 클래스가 구현할 때 classstatic 키워드 접두사를 붙여도 그대로 적용된다.

protocol SomeProtocol {
  static func someTypeMethod()
}
  • 단일 인스턴스 메서드 필수 조건을 가진 프로토콜의 정의는 다음과 같다.
protocol RandomNumberGenerator {
  func random() -> Double
}
  • 위 프로토콜을 채택하는 좀 더 복잡한 예제는 다음과 같다.
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())")
// "Here's a random number: 0.3746499199817101" 를 인쇄함
print("And another one: \(generator.random())")
// "And another one: 0.729023776863283" 를 인쇄함

✏️ 변경 메서드 필수 조건

  • 메서드가 자신이 속한 인스턴스를 수정 (또는 변경 (mutate))것이 필요할 때가 있다. 값 타입의 인스턴스 메서드면 메서드의 func 키워드 앞에 mutating 키워드를 둬서 메서드가 자신이 속한 인스턴스 및 그 인스턴스에 있는 어떤 프로퍼티의 수정을 허용한다고 지시한다.

  • 프로토콜을 채택한 어떤 타입의 인스턴스든 변경할 의도로 프로토콜 인스턴스 메서드 필수 조건을 정의하는 것이라면, 프로토콜 정의 부분에서 mutating 키워드로 메서드를 표시한다. 이는 구조체 및 열거형이 프로토콜을 채택해서 그 메서드 필수 조건을 만족할 수 있게 한다.

프로토콜 인스턴스 메소드 필수 조건을 mutating 으로 표시한 경우, 그 메소드를 클래스가 구현할 땐 mutating 키워드를 작성할 필요가 없습니다. mutating 키워드는 구조체와 열거체만 사용합니다.

protocol Togglable {
  mutating func toggle()
}
  • 구조체나 열거형이 Togglable 프로토콜을 구현하면, mutating 으로도 표시한 toggle() 메소드를 구현함으로써 그 구조체나 열거체가 프로토콜을 준수할 수 있다.
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 는 이제 .on 과 같음

✏️ 이니셜라이저 필수 조건

  • 프로토콜은 준수 타입에게 특정 이니셜라이저의 구현을 요구할 수 있다. 이 이니셜라이저들은 보통의 이니셜라이저와 정확히 똑같은 방식으로 프로토콜 정의 부분에 구현하지만, 중괄호나 메서드 본문이 없다.
protocol SomeProtocol {
  init(someParameter: Int)
}
프로토콜 이니셜라이저 필수 조건의 클래스 구현
  • 프로토콜 이니셜라이저 필수 조건은 준수 클래스에서 지명 이니셜라이저나 편의 이니셜라이저 어떤 것으로든 구현할 수 있다. 두 경우 모두, 반드시 required 수정자로 이니셜라이저 구현을 표시해야 한다.
class SomeClass: SomeProtocol {
  required init(someParameter: Int) {
    // 초기자 구현은 여기에 쓴다.
  }
}
  • required 수정자를 사용하면 준수 클래스의 모든 하위 클래스도 이니셜라이저 필수 조건의 명시적 또는 상속 구현을 제공하도록 하여 이들의 프로토콜 준수도 보장한다.

final 수정자로 표시한 클래스는 프로토콜 초기자 필수 조건에 required 수정자를 표시할 필요가 없는데, 최종 클래스는 하위 클래스를 만들 수 없기 때문입니다.

  • 하위 클래스가 상위 클래스의 지명 이니셜라이저를 재정의하면서 프로토콜의 일치하는 이니셜라이저 필수 조건도 구현한다면 이니셜라이저 구현에 requiredoverride 수정자를 둘 다 표시한다.
protocol SomeProtocol {
  init()
}

class SomeSuperClass {
  init() {
    // 초기자 구현은 여기에 쓴다.
  }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
  // "required" 는 SomeProtocol 준수에서; "override" 는 SomeSuperClass 에서 온 것임
  required override init() {
    // 초기자 구현은 여기에 쓴다.
  }
}
실패 가능한 이니셜라이저 필수 조건
  • 프로토콜은 준수 타입을 위한 실패 가능 이니셜라이저 필수 조건을 정의할 수 있다.

  • 실패 가능 이니셜라이저 필수 조건은 준수 타입에서 실패 가능 또는 실패하지 않는 이니셜라이저로 만족할 수 있다. 실패하지 않는 이니셜라이저 필수 조건은 실패하지 않는 이니셜라이저 또는 암시적으로 랩핑을 푸는 실패 가능 이니셜라이저로 만족할 수 있습니다.

✏️ 타입으로써의 프로토콜

  • 프로토콜은 실제로 그 자체론 어떤 기능도 구현하지 않는다. 그럼에도 불구하고, 코드에서 완전히 독립된 타입인 것 처럼 프로토콜을 사용할 수 있다. 프로토콜을 타입으로 사용하는 것을 실존 타입 (existential type) 이라고 할 때가 있는데, 이는 “프로토콜을 준수하는 타입 T 제로 재한다” 라는 문장에서 비롯한 것이다.

  • 다음 목록을 포함한 다른 타입이 되는 많은 곳에서 프로토콜을 사용할 수 있다.

    • 함수나, 메소드, 또는 초기자의 매개 변수 타입 및 반환 타입으로
    • 상수나, 변수, 또는 속성의 타입으로
    • 배열이나, 딕셔너리, 또는 다른 컨테이너의 항목 타입으로

프로토콜은 타입이기 때문에, (FullyNamedRandomNumberGenerator 같이) 이름을 대문자로 시작하여 (Int, String, 및 Double 같은) 스위프트의 다른 타입 이름과 일치하도록 합니다.

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 클래스를 사용하여 LinearCongruentialGenerator 인스턴스를 자신의 난수 발생기로 가지는 6-면체 주사위의 생성 방법이다.
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) 은 클래스나 구조체가 자신의 책임 일부를 또 다른 타입의 인스턴스로 넘길 수 있게 해주는 디자인 패턴 이다. 이 디자인 패턴은 맡길 책임을 은닉한 프로토콜 정의로 구현해서, 준수 타입(일을 맡은 자)이 자신이 맡은 기능을 제공한다는 걸 보증한다. 맡김은 특별한 한 행동에 응답하거나, 외부 소스의 실제 타입을 모르고도 그 소스에서 자료를 가져오는데 사용할 수 있다.
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)
}

💎 익스텐션으로 프로토콜 준수성 추가하기

  • 기존 타입의 소스코드에 접근할 수 없는 경우에도, 기존 타입을 확장하면 새로운 프로토콜을 채택하고 준수할 수 있다. 익스텐션 (extensions) 은 기존 타입에 새 프로퍼티, 메서드 및 첨자를 추가할 수 있으므로 프로토콜이 요구할 수 있는 모든 요구사항을 추가할 수 있다.

익스텐션에서 인스턴스의 타입에 프로토콜 준수성을 추가할 때 타입의 기존 인스턴스는 자동으로 그 프로토콜을 채택하고 준수합니다.

  • 예를 들어, 어떤 타입이든 Flyable 이라는 다음 프로토콜을 채택하여 구현하면, 날아가거나 걸어다닐 수 있다. 이는 날아간다는 동작과 걸어간다는 동작을 정의하는 메서드이다.
protocol Flyable {
    var name: String { get }
func fly()

}

- 다음의 `Bird`구조체를 확장하여 `Flyable`을 채택하고 준수하게 할 수 있다.
```swift
struct Bird {
    let name: String
}

extension Bird: Flyable {
    func fly() {
        print("\(name)이(가) 날아갑니다.")
    }
}
  • 이 익스텐션은 마치 Person, Bird구조체가 자신의 원본 구현에서 새로운 프로토콜을 제공한 것과 정확히 똑같은 방식으로 이를 채택한다. 프로토콜 이름은 콜론으로 구분하여 타입 이름 뒤에 작성하며, 프로토콜의 모든 필수 조건은 익스텐션 중괄호 안에 구현된다.

  • 또한 기존 타입에 구현되어있는 프로퍼티나 메서드를 활용할 수도 있다.

  • 어떤 Person, Bird의 인스턴스든 이제 Flyable 혹은 Walkable로 취급할 수 있다.

let bird: Bird = Bird(name: "Sparrow")

bird.fly()
// Sparrow이(가) 날아갑니다.

✏️ 조건부로 프로토콜 준수하기

  • 제네릭 타입의 일반화 매개 변수가 프로토콜을 준수할 때와 같이, 특정 조건 하에서만 프로토콜의 필수 조건을 만족할 수도 있다. 제네릭 타입이 조건부로 프로토콜을 준수하게 하려면 타입을 확장할 때 구속 조건을 나열하면 된다. 이러한 구속 조건을 채택할 프로토콜 이름 뒤에 작성하는 것은 제네릭의 where로 작성한다.

  • 다음 익스텐션은 저장한 원소의 타입이 Flyable을 준수할 때 마다 Array 인스턴스가 Flyable프로토콜을 준수하게 한다.

extension Array: Flyable where Element: Flyable {
    func fly() {
        for element in self {
            print("\(element)이(가) 날아갑니다.")
        }
    }
}

let sparrow = Bird(name: "Sparrow")
let pigeon = Bird(name: "Pigeon")
let owl = Bird(name: "Owl")

let typeOfBirds = [sparrow, pigeon, owl]
typeOfBirds.fly()

// Bird(name: "Sparrow")이(가) 날아갑니다.
// Bird(name: "Pigeon")이(가) 날아갑니다.
// Bird(name: "Owl")이(가) 날아갑니다.

✏️ 익스텐션으로 프로토콜 채택 선언하기

  • 타입이 프로토콜의 모든 필수 조건을 이미 준수하고 있지만, 그 프로토콜을 채택한다고 아직 알리지 않은 경우라면, 빈 익스텐션으로 프로토콜을 채택하게 할 수 있다.
struct Plane: Flyable {
    let name: String
    
    func fly() {
        print("쓩~ 날아갑니다.")
    }
}
extension Plane: Flyable { }
  • 이제 Flyable프로토콜이 필수 타입인 곳마다 Plane인스턴스를 사용할 수 있다.
let f22 = Plane(name: "F-22 Raptor")
let f35 = Plane(name: "F-35 Lightning II")
let su57 = Plane(name: "Su-57 Felon")
let kf21 = Plane(name: "KF-21 보라매")

let fiveGenerationFighter = [f22, f35, su57, kf21]
fiveGenerationFighter.fly()

// Plane(name: "F-22 Raptor")이(가) 날아갑니다.
// Plane(name: "F-35 Lightning II")이(가) 날아갑니다.
// Plane(name: "Su-57 Felon")이(가) 날아갑니다.
// Plane(name: "KF-21 보라매")이(가) 날아갑니다.

프로토콜의 필수 조건을 만족한다고 해서 타입이 이를 자동으로 채택하진 않습니다. 반드시 자신이 프로토콜을 채택한다는 걸 명시적으로 선언해야 합니다.

0개의 댓글