티스토리 뷰

Swift 공식 가이드/Swift 2

Protocols (2)

찜토끼 2016. 6. 19. 10:14

Swift 3.0.1 가이드에 대응하는 정리글을 작성하였습니다!!!

Protocols 정리 최신버전 > http://wlaxhrl.tistory.com/58





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



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


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



Delegation

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

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

예제를 통해 알아보자. 다음은 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 = [Int](count: finalSquare + 1,  repeatedValue: 0)
        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

직접 소스코드에 접근할 수 없는 타입이라도 확장을 통해 새로운 프로토콜을 따르게 할 수 있다. 확장을 통해 기존 타입에 새로운 프로퍼티, 메서드, 서브스크립트를 더할 수 있기 때문이다.

<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) // "25개의 말판을 가지고 하는 뱀과 사다리 게임"



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

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

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

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

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



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

프로토콜 타입도 다른 타입과 마찬가지로 콜렉션에 저장될 타입으로 사용될 수 있다. 예제를 보자.

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

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의 요구조건을 만족시킬 수 없다
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/30

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

Generics  (3) 2016.07.10
Protocols (3)  (0) 2016.06.19
Protocols (1)  (0) 2016.06.18
Extensions  (0) 2016.06.12
Nested Types  (0) 2016.06.07
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함