Swift - 22. 프로토콜 Protocols

지우개·2022년 4월 22일
0

Swift study

목록 보기
11/15
post-thumbnail

프로토콜이란?
특정 기능 수행에 필수적인 요소를 정의한 청사진

  • 프로토콜을 만족시키는 타입을 프로토콜을 따른다(conform)고 함.
  • 확장도 가능

프로토콜 문법

  • 프로토콜 정의
protocol SomeProtocol {
    // protocol definition goes here
}
  • 프로토콜을 따르는 타입 정의
struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}
  • subclass의 경우 superclass를 프로토콜 앞에 적어 준다.

요구사항

프로퍼티 요구사항 Property Requirements

  • 프로토콜에서는 프로퍼티가 stored property인지 computed property인지 명시하지 않음.
  • 프로퍼티 이름, 타입, gettable, settable 여부는 명시함.
  • 필수 프로퍼티는 항상 var로 선언해야 함.
protocol SomeProtocol {
    var mustBeSettable: Int { get set } // gettable과 settable 프로퍼티
    var doesNotNeedToBeSettable: Int { get } // gettable 프로퍼티
}

// 타입 프로퍼티는 static 키워드로 선언
protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}
  • 예제
// 하나의 프로퍼티를 갖는 프로토콜 선언
protocol FullyNamed {
    var fullName: String { get }
}

// 이 프로토콜을 따르는 구조체를 선언한다. 
// stored property로 사용
struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed"

// computed property로 사용
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"

메소드 요구사항 Method Requirements

  • 프로토콜은 준수하는 타입에 의해 구현되기 위해 인스턴스 메소드와 타입 메소드를 요구할 수 있다.
  • 프로토콜에서 필수 인스턴스 메소드와 타입 메소드를 명시할 수 있다.
  • 하지만 메소드 파라미터의 기본값은 프로토콜 안에서 사용할 수 없다.
protocol SomeProtocol {
    static func someTypeMethod()
}
  • 필수 메소드 지정시 함수명과 반환값을 지정할 수 있음
protocol RandomNumberGenerator {
    func random() -> Double
}

// 필수 메소드 random()를 구현한 클래스
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 키워드(func 키워드 앞)를 사용해 메소드가 속한 인스턴스와 인스턴스의 모든 프로퍼티를 수정 가능함을 표시할 수 있음 - 값 타입(구조체와 열거형) 형에만 사용

프로토콜에 mutating을 명시한 경우 이 프로토콜을 따르는 클래스 형을 구현할 때는 메소드에 mutating을 명시하지 않아도 된다.

예제

// 프로토콜 정의 
protocol Togglable {
    mutating func toggle()
}

// 이 프로토콜을 따르는 값타입 형에서 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 is now equal to .on


초기화 구문 요구사항 Initializer Requirements

  • 프로토콜에서 필수로 구현해야 하는 이니셜라이저 지정 가능
protocol SomeProtocol {
    init(someParameter: Int)
}

클래스에서 프로토콜 필수 이니셜라이저의 구현 Class Implementation of Protocol Initializer Requirements

  • 프로토콜에서 특정 이니셜라이저가 필요하다고 명시했기 때문에 구현에서 해당 이니셜라이저에 required 키워드를 붙여줘야 함
  • required 키워드를 사용하면 준수하는 클래스의 모든 하위 클래스에 이니셜라이저 요구사항의 명시적 또는 상속된 구현을 제공하여 프로토콜을 준수할 수 있음
class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // initializer implementation goes here
    }
}

final 클래스는 하위 클래스가 될 수 없으므로 required 키워드를 표시하지 않아도 됨

  • 특정 프로토콜의 필수 이니셜라이저를 구현하고, 수퍼클래스의 이니셜라이저를 서브클래싱하는 경우 이니셜라이저 앞에 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
    }
}
  • 프로토콜에서 실패가능한 이니셜라이저를 선언할 수 있음


타입으로써의 프로토콜 Protocols as Types

  • 프로토콜도 하나의 타입으로 사용
  • 다음과 같이 타입 사용이 허용되는 모든 곳에 프로토콜 사용 가능
    • 함수, 메소드, 이니셜라이저의 파라미터 타입 혹은 리턴 타입
    • 상수, 변수, 프로퍼티의 타입
    • 컨테이너인 배열, 사전 등의 아이템 타입
  • 프로토콜은 타입이므로 첫 글자를 대문자로 적어준다. ex) FullyNamed, RandomNumberGenerator

주사위 예제

  • 얼마나 많은 면을 가지고 있는지 - sides
  • 주사위 굴린 값 생성 - 난수 생성기를 제공하는 generator
class Dice {
    let sides: Int
    let generator: RandomNumberGenerator // generator 상수의 타입으로 사용
    init(sides: Int, generator: RandomNumberGenerator) { // 이니셜라이저의 파라미터형으로 사용
        self.sides = sides
        self.generator = generator
    }
    func roll() -> Int {
        return Int(generator.random() Double(sides)) + 1
    }
}

// Dice를 초기화할 때 generator 파라미터 부분에 RandomNumberGenerator 프로토콜을 따르는 인스턴스를 넣는다.
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

위임이란?

  • 클래스 또는 구조체가 책임의 일부를 다른 타입의 인스턴스에 넘겨주거나 위임할 수 있도록 하는 디자인 패턴
    이 디자인 패턴은 위임된 기능을 제공하기 위해 준수하는 타입(대리자)이 보장되도록 위임된 책임을 캡슐화하는 프로토콜을 정의하여 구현한다. 위임은 특정 작업에 응답하거나 해당 소스의 기본 타입을 알 필요 없이 외부 소스에서 데이터를 검색하는데 사용할 수 있다.

주사위 기반 보드게임 예제

// 2가지 프로토콜 정의하기
protocol DiceGame { // 주사위를 포함하는 모든 게임에 채택될 수 있는 프로토콜
    var dice: Dice { get }
    func play()
}
// 실제 DiceGame의 행위와 관련된 구현을 DiceGameDelegate를 따르는 인스턴스에 위임
protocol DiceGameDelegate: AnyObject { // DiceGame의 진행사항을 추적하기 위해 채택
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

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

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

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

익스텐션을 이용해 프로토콜 추가 Adding Protocols Conformance with an Extension

이미 존재하는 타입에 새 프로토콜을 따르게 하기 위해 익스텐션을 사용할 수 있음

protocol TextRepresentable {
    var textualDescription: String { get }
}
// 익스텐션을 이용해 Dice를 TextRepresentable 프로토콜을 따르게 하기
extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

조건적으로 프로토콜 준수 Conditionally Conforming to a Protocol

  • 특정 조건을 만족시킬때만 프로토콜을 따르도록 제한할 수 있음
  • where절을 사용해 정의

예제
TextRepresentable을 따르는 Array 중에 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]"

익스텐션을 이용해 프로토콜 채용 선언 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"

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


합성된 구현을 사용하여 프로토콜 채택 Adopting a Protocol Using a Synthesized Implementation

Swift는 다음과 같은 사용자 정의 타입에 대해 Equatable의 합성된 구현을 제공

  • Equatable 프로토콜을 준수하는 저장된 프로토콜만 있는 구조체
  • Equatable 프로토콜을 준수하는 연관된 타입만 있는 열거형
  • 연관된 타입이 없는 열거형

Swift는 다음과 같은 사용자 정의 타입에 대해 Hashable에 합성된 구현을 제공

  • Hashable 프로토콜을 준수하는 저장된 프로퍼티만 가지는 구조체
  • Hashable 프로토콜을 준수하는 연관된 타입만 가지는 열거형
  • 연관된 타입이 없는 열거형

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

프로토콜은 배열, 딕셔너리와 같은 콜렉션에 저장되기 위해 타입으로 사용될 수 있음.


프로토콜 상속 Protocol Inheritance

프로토콜은 하나 이상의 다른 프로토콜을 상속할 수 있고 상속한 요구사항 위에 요구사항을 더 추가할 수 있음.
콤마(,)로 구분하여 여러 개의 상속된 프로토콜을 목록화할 수 있음

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

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

프로토콜 채택을 프로토콜의 상속 목록에 AnyObject 프로토콜을 추가하여 구조체 또는 열거형이 아닌 클래스 타입으로 제한할 수 있음.

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

참조 구문만 필요한 프로토콜을 정의하기 위해 클래스 전용 프로토콜을 사용함


프로토콜 합성 Protocol Composition

동시에 여러 프로토콜을 따르는 타입 선언

예제) Person은 Named와 Aged 프로토콜을 동시에 따르는 구조체
wishHappyBirthday 메소드의 celebrator 파라미터는 Named 프로토콜과 Aged 프로토콜을 동시에 따르는 타입으로 선언하기 위해 Named & Aged로 표시함

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!"

예제) Location 프로토콜과 위의 Named 프로토콜을 따르는 City 클래스를 구현한 예

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!"

프로토콜 순응 확인 Checking for Protocol Conformance

  • is 연산자를 이용하면 어떤 타입이 특정 프로토콜을 따르는지 확인할 수 있음. bool
  • as?는 특정 프로토콜 타입을 따르는 경우 그 옵셔널 타입의 프로토콜 타입으로 다운캐스트를 하게 되고 따르지 않는 경우는 nil을 반환
  • as!는 강제로 특정 프로토콜을 따르도록 정의함. 만약 다운캐스트에 실패하면 런타임 에러 발생
protocol HasArea {
    var area: Double { get }
}
// HasArea 프로토콜을 따르는 Circle 클래스와 Country 클래스를 선언한다.
// Circle 클래스에서는 area 프로퍼티를 계산된 프로퍼티로 구현했고, 
class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi radius radius }
    init(radius: Double) { self.radius = radius }
}
// Country 클래스에서는 저장 프로퍼티로 구현한다.
class Country: HasArea {
    var area: Double
    init(area: Double) { self.area = area }
}
// HasArea를 따르지 않는 Animal이라는 클래스 선언
class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}
// Circle, Country, Animal의 인스턴스를 objects라는 배열에 넣는다.
let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]
// objects 배열을 순회하며 as? HasArea 구문을 사용해 HasArea 프로토콜을 따르는지 확인하고 따르는 경우 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를 따르기 때문에 area값이 반환되고
// 마지막 인스턴스는 HasArea를 따르지 않기 때문에 else 절이 실행된다.

옵셔널 프로토콜 요구조건 Optional Protocol Requirements

프로토콜을 선언하면서 필수 구현이 아닌 선택적 구현 조건을 정의할 수 있다.
이 프로토콜의 정의를 위해서 @objc 키워드를 프로토콜 앞에 붙이고, 개별 함수 혹은 프로퍼티에는 @objc와 optional 키워드를 붙인다.
@objc 프로토콜을 클래스 타입에서만 채용될 수 있고 구조체나 열거형에서는 사용할 수 없음.

예제) 두 가지 선택적 구현을 할 수 있는 CounterDataSource 프로토콜

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

CounterDataSource를 따르면서 구현은 하나도 하지 않는 클래스를 선언할 수는 있지만, 좋은 방법은 아니다. 만약 하나도 구현하지 않는다면 필요하지 않다는 것이므로 굳이 그렇게 선언할 필요는 없다.

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

프로토콜 익스텐션 Protocol Extensions

  • 익스텐션을 이용해 프로토콜을 확장할 수 있음
  • 익스텐션을 이용해 구현을 추가할 수는 있어도 다른 프로토콜로 확장/상속할 수는 없음.
    • 만약 그렇게 한다면 익스텐션이 아니라 프로토콜 자체에 구현해야 함
// random()을 따르는 RandomNumberGenerator에 randomBool()을 따르도록 추가한 예
extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

기본 구현 제공 Providing Default

기본 구현을 제공하는데 익스텐션을 사용할 수 있음
(특정 프로토콜을 따르는 타입 중에서 그 프로토콜의 요구사항에 대해 자체적으로 구현한게 있으면 그것을 사용하고 아니면 기본 구현 사용)
프로토콜에서는 선언만 할 수 있는데 익스텐션을 이용해 기본 구현을 제공할 수 있음

프로토콜 익스텐션에 의해 구현된 기본 구현은 옵셔널 요구조건과 다르다.
공통점 - 둘 다 꼭 구현하지 않아도 됨. 차이점 - 사용 시 기본 구현은 옵셔널 체이닝을 사용하지 않아도 됨.

예제) PrettyTextRepresentableprettyTextualDescription 프로퍼티의 구현을 그냥 textualDescription을 반환하도록 구현한 예

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

프로토콜 익스텐션에 제약 추가 Adding Constraints to Protocol Extensions

프로토콜 익스텐션이 특정 조건에서만 적용되도록 선언할 수 있음 - where 절 이용

예제) Collection 엘리먼트가 Equatable인 경우에만 적용되는 allEqual() 메소드를 구현한 예

extension Collection where Element: Equatable {
    func allEqual() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]

// 각 배열의 엘리먼트는 모두 Equatable 프로토콜을 따르기 때문에 allEqual()을 호출할 수 있음
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

0개의 댓글