momodudu.zip

#7 Swift - Protocol(1) 본문

ios/Swift

#7 Swift - Protocol(1)

ally10 2019. 11. 28. 21:36

Swift에서 Protocol이란 말 그대로 일종의 "약속"이라고 보면 된다.

 

Language guide에 따르면, 특정 task나 기능등을 만족시키라고 요구사항을 작성하는 blueprint라고 정의되어 있다.

간단하게 말하자면, 이러이러한 기능이나 property를 꼭 구현해라. 라고 정의하는 것이다.

 

protocol은 class, struct, enum에 사용할 수 있다.

Protocol의 Syntax

protocol SomeProtocol{
	// some definitions
}
    
 struct SomeStrcut: SomeProtocol{
        
 }

 

protocol keyword로 프로토콜을 정의하고, 상속하듯이 protocol을 conformation하겠다고 명시해준다.

 

Protocol의 Requirements

protocol에서 구현해라고 지시할 수 있는것에 몇가지 종류가 있다.

- Property

- Method

- Mutating Method

- Initializer

 

1. Property

protocol FullName{
    // read-only (stored or computed) property
    var name: String { get }
}

property를 protocol에 넣으려면 위와 같이 정의하면 된다.

name이라는 이름의 String type의 read-only property 가 정의되어 있다.

 

이 property는 stored여도 되고, computed property로 구현해도 된다. 

즉, 이 프로콜을 준수하는 객체들은 name: String의 read only property만 존재하면 된다.

 

struct Person: FullName{
    var name: String // conform to protocol as stored property
    init(name: String){ self.name = name }
}

 

위 예제는 FullName Protocol을 name 이라는 Property를 넣으면서 지켰는데,

이니셜라이저에서 값을 받아서 "저장"만 하는 stored property로 구현되어 있다.

 

struct Person: FullName{
    var firstName: String
    var secondName: String
    init(firstName: String, secondName: String){
        self.firstName = firstName
        self.secondName = secondName
    }
    var name: String {
        return firstName + " " + secondName
    }
}

 

위 예제는 이니셜라이저에서 firstName, SecondName을 받아서 두개의 조합을 name으로 리턴하는 computed property로 구현되었다.

 

즉, requirement로서의 property는 이 property를 쓸것이다. 라는 정의만 해놓는 것이지

그걸 stored로 쓸지, computed로 쓸지는 알아서 구현하면 된다.

 

2. Method

protocol에서 특정 메소드를 구현하도록 규칙을 만들 수 있다.

protocol RandomNumberGenerator{
    func random() -> Double
}

Method 역시 함수 body는 구현하지 않고, 함수 이름, 리턴타입, paramter만 정의한다.

Variadic parameter도 허용하는데, default parameter는 허용하지 않는다.

 

위 예제에서는 랜덤 넘버를 생성하는 프로토콜을 정의하고, 리턴 타입은 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
    }
}

넘버를 어떻게 생성하는지는 중요하지 않고, 프로토콜을 adopt하고 random()->Double method에 구현을 했다.

 

이제 RamdomNumberGenerator 프로토콜을 채택한 클래스가 구체화 되었다.

 

3. Mutating Method

 

위 예시에서는 클래스가 프로토콜을 adopt 했기 때문에 메소드 내에서 Property 변경이 가능하지만

struct, enum의 경우 method에서 property 변경이 불가능하므로 이 경우 프로토콜에도 mutating keyword를 명시해주어야 한다.

 

protocol MutatingProtocol{
	mutating func FuncName()
}

 

4. Initializer

struct와 enum은 상속의 개념이 없으므로 init을 protocol 그대로 준수하면 된다.

단, class에서는 init을 designated / convenience init으로 넣을 수 있는데 이 둘 다 protocol에 명시해 둘 수 있다.

 

대신 class가 adopt한 protocol의 initailizer에서는 subclass에서 이 init을 구현할 수 있도록

required keyword를 붙여주어야 한다. 안하면 컴파일 에러가 뜬다.

물론 프로토콜을 채택한 클래스에 더이상 오버라이딩 되지 않을거라는 final keyword가 붙어있다면 필요 없다.

 

protocol SomeProtocol{
	init()
}

class SomeClass: SomeProtocol{
	required init(){ 
    	// ...
    }
}

 

만약 어떤 클래스가 protocol을 채택했고, 또 다른 클래스를 상속받아서 init을 overriding 해야한다면

 

protocol SomeProtocol{
	init()
}

class SomeSuperClass(){
	required init()
}

class SomeClass: SomeSuperClass, SomeProtocol{
	required override init()
}

SuperClass와 Protocol을 모두 지켜야하는 SomeClass는 protocol로부터 required, super class로부터 override keywrod를 받아서 위와 같이 init을 작성해야 한다.

 

Failable initializer도 protocol에 추가할 수 있다.

 

 

type으로서의 Protocol

protocol도 하나의 type이므로 parameter, constant, variable, property등으로 쓰일 수 있고 Array나 Dictionary의 Element type으로써쓰일수도 있다.

class Dice{
    let sides: Int
    let generator: RandomNumberGenerator
    
    init(side: Int, generator:RandomNumberGenerator){
        self.sides = side
        self.generator = generator
    }
    
    func roll() -> Int{
        return Int(generator.random() * Double(sides))+1
    }
}

위에서 선언했던 랜덤 넘버를 생성하는 protocol RandomNumberGenerator type의 property를 갖는 클래스 Dice 예제이다.

주사위에 대한 클래스이고, 주사위의 면 개수와 random number를 생성할 Generator protocol을 property로 갖는다

그리고 이 두 property를 init에서 받아서 저장한다.

 

실제로 이 Dice를 사용하는 코드를 보면,

var dice = DIce(side:6 , generator: LinearCongruentialGenerator())

dice instance를 생성할 때 init의 인자로 RandomNumberGenerator Protocol 이 아닌,

RandomNumberGenerator Protocol을 준수한 LinearCongruentialGenerator instance를 생성하여 전달해주고있다.

 

순수 정의만 따른다면 두개의 타입은 다른 타입이다. 그렇지만 LinearCongruentialGenerator는 Random protocol을 지키고 있고,

Linear instance를 인자로 전달하더라도 downcasting이 자동으로 이루어지면서 위와 같이 파라미터로 전달이 가능하다!

 

 

Delegation

Swift에서 delegation이란, responsibility 일부를 다른 instance로 hand-off하는 디자인 패턴의 한 종류라고 한다.

Protocol이 이 delegate 구현을 위해서 쓰인다고 한다. delegate할 역할을 프로토콜로 캡슐화해서 delegate 디자인 패턴을 구현한다.

 

무슨말인지 코드를 통해서 차근차근 ...보면 좋겠지만 가이드에 나와있는 예제도 사실 조금 복잡하다.

 

먼저, 두 개의 프로토콜을 정의한다.

// protocol for games involve dice
protocol DiceGame{
	
    var dice: Dice { get }
    func play()
}

// provides three methods to track the progress of the game
protocol DiceGameDelegate: AnyObject{

	// when a new game starts
    func gameDidStart(_ game: DiceGame)
    
    // when a new turn begins
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    
    // when the game ends
    func gameDidEnd(_ game: DiceGame)
}

주사위를 가지고 하는 게임에 대한 규칙, DIceGame protocol과 이 주사위 게임의 진행척도를 체크하는 역할을 캡슐화해서, 대신 진행 할 instance가 지켜야하는 규칙을 정의한 DiceGameDelegate protocol이다.

 

delegate뒤에 붙은 :AnyObject는 "이 프로토콜을 class만 채택했으면 좋겠어" 라고 명시해두는 것이다.

즉, struct나 enum은 이 프로토콜을 adopt 할 수 없다.

 

그리고 이 프로토콜을 채택한 두 가지 클래스도 정의한다.

 

class SnakesAndLadders: DiceGame{
    let finalSquare = 25
    
    // conform to protocol DiceGame( gettable property )
    let dice = Dice(side: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
    }
    
    // Optional DiceGameDelegate -> isn't requred to play game.
    // Thereafter, the game instantiator has the option to set the property to a suitable delegate.
    // Because the DiceGameDelegate protocol is class-only, you can declare the delegate to be weak to prevent reference cycles.
    weak var delegate: DiceGameDelegate?
    
    // conform to protocol DiceGame
    func play(){
        square = 0
        
        delegate?.gameDidStart(self)
        
        gameLoop: while square != finalSquare{
            let diceRoll = dice.roll()
            
            // optional chaining
            // when delegates? is nil, delegate calls will be failed.
            // when delegates? is non-nil, pass SnakeAndLadders Instance(itself) as a parameter
            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)
    }
}

 

예제코드 치고는 조금 길지만 한 단위씩 차근차근 보면 된다...

먼저 DiceProtocol을 채택한 SnakesAndLadders 라는 class이다. 이는 프로토콜을 지켜야하므로  dice property와 play method는 구현되어 있어야 한다.

 

 property dice는 위 예제에서처럼 RandomNumberGenerator Protocol을 준수하는 LinearCongruentialGenerator를 사용한다.

func play() 는 게임 진행상황을 체크하면서 주사위로 생성된 랜덤 숫자로 25까지 도달하면 끝나도록 설계되어 있다.

즉, 사다리를 만나면 +n 만큼 더 이동이 가능하고, 뱀을 만나면 -n 만큼 이동하는 게임이다.

사다리와 뱀에 대한 정의가 stored property board에 정의되어 있고..

 

그리고 특이한점이, delegate이라는 Optional DiceGameDelegate type의 property이다.

이 delegate가 게임의 진행상황을 체크하는 역할을 위임받아 대신 진행해줄 delegate이다.

아직 인스턴스 생성은 안했으나, DiceGameDelegate protocol을 준수하는 어떤 타입의 delegate가 들어가도 상관 없다.

 

SnakesAndLadders에 delegate가 옵셔널인 이유는, delegate가 세팅되지 않은 상태라면 게임 진행이 되지 않게 하기 위함이다.

즉, deletage를 옵셔널 타입으로 지정함으로써 코드상에는 optional chaining 처리를 해서,

옵셔널 값이 들어있을때만 제대로 동작하도록 정의하기 위함이다.

 

// Adopt DiceGameDelegate
class DiceGameTracker: DiceGameDelegate{
    var numberOfTurns = 0
    
    // game access to dice.
    // game conforms to DiceGame Protocol, so it's guranteed it has dice property
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        
        if game is SnakesAndLadders {
            print("start to play SnakeAndLadders")
        }
        
        // use extension property(textualDescription) conforming to TextRepresentable protocol
        print("the game uses \(game.dice.textualDescription)")
    }
    
    // type of the 1st param is DiceGame, and SnakesAndLadder is passed(underlying type -> downCasting)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("Rolled a \(diceRoll)")
    }
    
    func gameDidEnd(_ game: DiceGame) {
        print("The game lasted for \(numberOfTurns)")
    }
}

 

 

위와 같은 delegate를 구체화한 DiceGameTracker로 프로토콜에 정의된 메소드들을 구체화했다.

game이 시작되면 여태까지 진행한 턴 수를 초기화하는 gameDidStart,

game이 진행될때마다 한 턴씩 +1을 하는 game

game이 끝나면 메세지를 출력하는 gameDidEnd method.

 

gameDidStart는 인자로 DiceGame을 받아 if game is SnakeAndLadders를 체크한다.

is operator는 같은 타입인지 체크하는 operator이다.

 

즉, 인자로 받은 game이 SnakeAndLadders인지 체크하고 맞다면 start message를 출력한다.

사실 코드만으로 봤을때 인자로 넘어온 game이 사다리게임인지 뭔지 delegate입장에선 알 수 없다.

다만 DiceGame의 protocol을 지킨 어떤 한 인스턴스가 들어올것이고, 그 인스턴스의 타입이 snake인지 체크하는것이다.

 

이 부분이 delegate의 핵심이라고 한다.

즉, 근간이 되는 타입에 대한 명확한 정보 없이도 특정 액션에 대한 respond를 할 수 있는 것이다.

 

 

그 외에 SnakeAndLadders의 weak keyword는 순환참조 방지용 키워드인데, 이 내용에 대해서는 나~~중에 ARC 포스팅에서 다루기로한다.

 

헥헥... 프로토콜은 내용이 중요한만큼 분량도 많다.

다음 포스팅에서 프로토콜의 확장이나 상속에 대해서 포스팅 할 예정이다.

 

'ios > Swift' 카테고리의 다른 글

#9 Swift - class, struct에 대해서 다시 짚어보기  (0) 2020.04.23
#8 Swift - Automatic Reference Counting  (0) 2019.12.28
#6 Swift - Extension  (0) 2019.11.28
#5 Swift - Optional chaining  (0) 2019.11.27
#4 Swift - Initialization  (0) 2019.11.26