본문 바로가기
Swift 공식 가이드/Swift 3

Protocols (2)

by 토끼찌짐 2017. 3. 25.

Apple 제공 Swift 프로그래밍 가이드(2.2)의 Protocols 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.



프로토콜 2편입니다. 이번 시간에는 Delegation부터 시작합니다. 1편을 아직 안 읽으셨다면 먼저 1편을 읽어보세요.


Protocols 1편 링크 > http://wlaxhrl.tistory.com/57



Delegation

딜리게이션은 클래스/구조체가 자신의 책임 중 일부를 다른 타입의 인스턴스에게 떠넘길 수 있게 해주는(=위임할 수 있게 해주는 = hand off = delegate) 디자인 패턴이다.

딜리게이션 패턴을 구현하려면, 다른 타입에게 위임할 책임(기능)을 캡슐화하고 있는 프로토콜을 정의해야 한다. 그 프로토콜을 따르는 타입(conforming type)을 딜리게이트라고 하고, 딜리게이트는 위임받은 책임(기능)을 제공할 수 있다고 보장된다.

딜리게이션 패턴은 특정 액션에 대해 응답할 때, 또는 외부 소스로부터 어떠한 데이터를 받아와야 할 때 유용하게 사용할 수 있다. 이때 딜리게이션을 하는 곳(책임을 떠넘기는 곳)에서는 딜리게이트(책임을 위임받아 제공하는 곳)가 어떻게 응답을 할지, 어떻게 넘겨줄 데이터를 만드는지에 대해 몰라도 된다.

예제를 통해 알아보자. 다음은 1편에서 다뤘던 주사위(Dice) 클래스와, 주사위를 사용하는 보드게임에 쓰일 프로토콜 두 개 DiceGame, DiceGameDelegate이.

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

protocol DiceGame {
    var dice: Dice { get }
    func play()
}

protocol DiceGameDelegate {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}


이 프로토콜을 이용하여 Control Flow 장에서 다뤘던 뱀과 사다리 게임을 구현해보겠다.

// DiceGame 프로토콜을 따르는, 주사위 게임 "뱀과 사다리" 클래스이다.
// dice 프로퍼티와 play 메서드를 어떻게 구현하고 있는지 살펴보자.
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
    }
    
    var delegate: DiceGameDelegate?
    // 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)
    }
}


// DiceGameDelegate 라는 프로토콜을 따르는 클래스를 정의하여,
// 게임의 시작/진행도중/끝에서 내가 실행시키고 싶은 코드들을 작성한다.
// 예제의 경우에는 이 프로토콜을 이용해서 게임을 중계할 수 있게 하였다.
class DiceGameTracker: DiceGameDelegate {
    var numberOfTurns = 0
    
    func gameDidStart(_ game: DiceGame) {
        numberOfTurns = 0
        
        if game is SnakesAndLadders {
            print("뱀과 사다리 게임이 시작되었다")
        }
        
        print("이 게임은 \(game.dice.sides)면체 주사위를 사용한다")
    }
    
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
        numberOfTurns += 1
        print("주사위를 굴려 \(diceRoll)이 나왔다")
    }
    
    func gameDidEnd(_ game: DiceGame) {
        print("게임은 \(numberOfTurns)턴 만에 끝났다")
    }
}


// 실제로 이런 식으로 쓰인다
let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// 뱀과 사다리 게임이 시작되었다
// 이 게임은 6면체 주사위를 사용한다
// 주사위를 굴려 3이 나왔다
// 주사위를 굴려 5이 나왔다
// 주사위를 굴려 4이 나왔다
// 주사위를 굴려 5이 나왔다
// 게임은 4턴 만에 끝났다



Adding Protocol Conformance with an Extension

구현 소스코드를 건드릴 수 없는 타입(ex: Int 등)이라도 확장을 통해 새로운 프로토콜을 따르게 할 수 있다. 확장을 통해 기존 타입에 새로운 프로퍼티, 메서드, 서브스크립트를 더할 수 있기 때문이다.

<note> 확장을 통해 기존 타입이 새로운 프로토콜을 따르게 되면, 그 전에 생성되었던 그 타입의 인스턴스들도 자동으로 프로토콜을 따르게 된다.

예제를 통해 알아보자.

protocol TextRepresentable {
    var textualDescription: String { get }
}

// Dice 클래스를 확장하여 TextRepresentable 프로토콜을 따르게 해보자
extension Dice: TextRepresentable {
    var textualDescription: String {
        return "\(sides)면체 주사위"
    }
}

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription) // "12면체 주사위"


// SnakesAndLadders 클래스를 확장하여 TextRepresentable 프로토콜을 따르게 해보자
extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "\(finalSquare)개의 말판을 가지고 하는 뱀과 사다리 게임"
    }
}

let game = SnakesAndLadders()
print(game.textualDescription)
// print "25개의 말판을 가지고 하는 뱀과 사다리 게임"



<확장을 통해서 프로토콜 적용을 선언하기>

이미 프로토콜의 요구조건을 만족하고 있지만 프로토콜을 따른다고 명시를 하지 않은 경우, 빈 확장을 통해 프로토콜 적용을 선언하여 프로토콜을 따르게 할 수 있다.

// <주의> 프로토콜 따른다는 명시를 하지 않으면
// 아무리 요구사항을 만족하더라도 그 프로토콜을 따르는 것이 아니다
struct Hamster {
    var name: String
    var textualDescription: String {
        return "\(name)라는 이름의 햄스터"
    }
}

// 빈 확장 + 프로토콜 적용을 명시
extension Hamster: TextRepresentable {}

// 이제 Hamster 구조체는 TextRepresentable 프로토콜을 따른다
let hamster = Hamster(name: "햄토리")
let textRepresentable: TextRepresentable = hamster
print(textRepresentable.textualDescription)
// print "햄토리라는 이름의 햄스터"



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

1편에서 프로토콜 타입도 다른 타입과 마찬가지로 "타입"으로 취급된다고 했었다. 따라서 프로토콜 타입은 콜렉션에 저장될 타입으로도 사용될 수 있다. 예제를 보자. Objective-C 로 똑같은 코드를 작성하려면 id타입을 써야할텐데, 이런 부분에 있어서는 스위프트가 참 좋다는 생각이 든다. 프로토콜로 더 많은 것들을 깔끔하게 처리할 수 있을 것 같다.

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

for thing in things {
    print(thing.textualDescription)
}
// "25개의 말판을 가지고 하는 뱀과 사다리 게임"
// "12면체 주사위"
// "햄토리라는 이름의 햄스터"




프로토콜 상속

프로토콜은 하나 이상의 프로토콜을 상속받을 수 있다. 물론 상속받은 뒤 자신만의 요구사항을 더할 수도 있다. 상속 문법은 클래스 상속 문법과 유사하고, 여러 개의 프로토콜을 상속받을 때는 쉼표로 구분한다.

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 프로토콜 정의...
} 


TextRepresentable 프로토콜을 상속받는 프로토콜을 하나 살펴보자.

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
    
    // PrettyTextRepresentable 프로토콜을 따르기 위해서는
    // TextRepresentable의 요구사항인 textualDescription 프로퍼티와
    // PrettyTextRepresentable의 요구사항인 prettyTextualDescription 프로퍼티를
    // 모두 구현해야 한다
}


// 앞서 확장을 통해 SnakesAndLadders가 TextRepresentable를 따르게 했었다
// 만약 그 코드를 없애거나 주석처리 한다면
// 다음 코드만으로는 PrettyTextRepresentable의 요구조건을 만족시킬 수 없다
// prettyTextualDescription 프로퍼티는 제공하고 있지만
// textualDescription 프로퍼티를 제공하지 않기 때문이다
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
    }
}

print(game.prettyTextualDescription)
// "25개의 말판을 가지고 하는 뱀과 사다리 게임"
// "○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○"



Class-Only 프로토콜

오직 클래스에 한정한 프로토콜을 정의할 수 있다. (구조체나 ENUM은 그 프로토콜을 적용할 수 없도록) 프로토콜을 정의할 때 상속 리스트에 class 키워드를 추가하면 된다. 이때 class 키워드는 항상 상속 리스트의 처음에 와야 한다.

// 이 프로토콜을 구조체나 ENUM에 적용하려고 하면 컴파일 에러가 난다
protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // 프로토콜 정의는 여기서부터
}

<note> 프로토콜의 요구사항이 reference 타입을 가지고 동작하는 것을 가정할 경우 class-only 프로토콜을 쓰도록 하라.




이번 편에서는 딜리게이션 패턴, 확장을 통해 프로토콜을 적용하는 법 등을 알아보았다. 다음 3편에서는 프로토콜 합성부터 시작해서 나머지 사항들에 대해 알아보자.


 3편을 작성하였습니다. http://wlaxhrl.tistory.com/59

'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글

Generics  (0) 2017.03.29
Protocols (3)  (2) 2017.03.25
Protocols (1)  (0) 2017.03.23
Extensions  (1) 2017.03.23
Nested Types  (0) 2017.03.23