티스토리 뷰
Update Note: This tutorial has been updated to Swift 3.0 by Niv Yahel. The original tutorial was written by Erik Kerber.
레이싱 게임을 개발한다고 상상해보세요. 자동차를 운전하거나, 오토바이를 타거나, 비행기를 조종 할 수 있습니다. 이러한 유형의 응용 프로그램을 만드는 일반적인 방법은 객체 지향 설계를 사용하여 유사성을 공유하는 모든 객체에 상속 된 객체 내부의 모든 로직을 캡슐화하는 것입니다.
이 설계 방식은 효과적이지만, 몇가지 단점이 있습니다. 만약 여러분이 가스를 필요로 하는 것을 필요로 하거나, 배경에서 날아다니는 새들을 위한 날아다니는 것을 만들어 낼 수 있는 기계를 만든다면, 자동차의 기능적 구성 요소를 재사용 할 수 있는 좋은 방법이 없습니다.
이 시나리오에서 프로토콜이 진정으로 빛납니다.
Swift는 프로토콜을 사용하여 기존 클래스, 구조체 및 열거형의 인터페이스 명세를 할 수 있도록 해줍니다. 이를 통해 일반적으로 상호작용을 할 수 있습니다.
Swift 2는 프로토콜을 확장하고 기본 구현을 제공하는 방법을 소개했습니다. 마지막으로, Swift 3는 연산자 적합성을 개선하고 이것을 표준 라이브러리의 새로운 숫자 프로토콜에 사용합니다.
프로토콜은 매우 강력하며 당신이 코드를 작성하는 방식을 변화시킬 수 있습니다.이 튜토리얼 에서는 프로토콜을 생성하고 사용할 수 있는 방법을 탐색하고, 프로토콜 지향 프로그래밍 패턴을 사용하여 코드를 더욱 확장성 있게 만들어 줄 것입니다.
또한 프로토콜 확장을 사용하여 Swift 표준 라이브러리 자체를 개선하고, 이것이 당신이 작성한 코드에 어떻게 영향을 미치는지 볼 수 있습니다.
Getting Started
새로운 playground를 만들어 보십시오. Xcode에서, File\New\Playground… 를 선택하고 이름을 SwiftProtocols로 하십시오. 이 튜토리얼의 모든 코드는 플랫폼에 독립적이므로 모든 플랫폼을 선택할 수 있습니다. 다음을 클릭하여 저장 위치를 선택하고 마지막으로 만들기를 클릭하십시오.
새로운 playground가 열리면, 다음 코드를 추가하십시오:
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
airspeedVelocity를 정의하는 Flyable
프로토콜 뿐만 아니라, 프로퍼티 이름과 canFly를 가진 간단한 프로토콜 Bird 정의합니다.
프로토콜 이전의 세계에서는, Flyable
을 기본 클래스로 시작한 다음, 객체 상속에 의존하여 Bird와 비행기와 같은 날아다니는 것들을 정의 할 수 있었습니다. 여기서, 모든 것이 프로토콜로 시작된다는 것에 주목하세요! 이를 통해 기본 클래스가 필요 없는 방식으로 기능을 캡슐화 할 수 있습니다.
다음 단계에서, 실제 타입을 정의하며 전체 시스템을 보다 유연하게 만들 수 있는 방법을 알아볼 수 있습니다.
Defining Protocol-Conforming Types
다음의 구조체 정의를 추가하세요. :
struct FlappyBird: Bird, Flyable {
let name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
return 3 * flappyFrequency * flappyAmplitude
}
}
Bird와 Flyable 프로토콜을 모두 준수하는 새로운 구조체 FlappyBird
를 정의합니다. airspeedVelocity
는 flappyFrequency
와 flappyAmplitude
로 계산됩니다.
다음으로, 다음과 같은 두 개의 구조체 정의를 추가합니다.:
struct Penguin: Bird {
let name: String
let canFly = false
}
struct SwiftBird: Bird, Flyable {
var name: String { return "Swift \(version)" }
let version: Double
let canFly = true
// Swift is FASTER every version!
var airspeedVelocity: Double { return version * 1000.0 }
}
Penguin
은 Bird
이지만, 날 수 없습니다. 아하! 상속으로 접근하지 않아 다행입니다. 프로토콜을 사용하면 기능 구성 요소를 정의하고, 관련 객체가 그것을 준수하게 할 수 있습니다.
당신은 약간 중복된 것을 보았을 겁니다. 모든 Bird 타입은 Flyable이더라도, canFly인지 아닌지 선언해야 합니다.
Extending Protocols With Default Implementations
프로토콜 확장을 사용하면, 프로토콜에 대한 기본 동작을 정의할 수 있습니다. Bird
프로토콜 정의 바로 아래에 다음을 추가합니다.:
extension Bird {
// Flyable birds can fly!
var canFly: Bool { return self is Flyable }
}
Flyable
타입일 경우, canFly
가 true
를 리턴하도록 기본 동작을 설정하는 Bird
의 확장을 정의합니다. 다르게말하면, 어떤 Flyable
bird라도 더이상 명시적으로 선언할 필요가 없다는 것입니다!
FlappyBird
, SwiftBird
그리고 Penguin
구조체에서 let canFly = ...
선언을 삭제하세요 . 프로토콜 확장이 이제 그 요구 사항을 처리하기 때문에 플레이그라운드가 성공적으로 빌드 된 것을 볼 수 있습니다.
Why Not Base Classes?
프로토콜 확장 및 기본 구현은 기본 클래스 또는 다른 언어의 추상클래스를 사용하는 것과 유사하지만, Swift에서는 몇가지 주요 이점을 제공합니다 :
- 타입은 하나 이상의 프로토콜을 준수할 수 있으므로, 여러 프로토콜에 의한 기본 동작으로 타입을 꾸밀 수 있습니다. 일부 프로그래밍 언어가 지원하는 클래스의 다중 상속과는 달리, 프로토콜 확장은 추가적인 상태를 도입하지 않습니다.
- 프로토콜은 클래스, 구조체, 열거형에 의해 채택될 수 있지만, 기본 클래스 및 상속은 클래스 유형으로 제한됩니다.
다르게 말하면, 프로토콜 확장은 클래스 뿐만 아니라 값 유형에 대한 기본 동작을 정의하는 기능을 제공합니다.
이전에 보았듯, 구조체에 이것을 사용하였습니다.
다음과 같은 열거형을 추가합니다. :
enum UnladenSwallow: Bird, Flyable {
case african
case european
case unknown
var name: String {
switch self {
case .african:
return "African"
case .european:
return "European"
case .unknown:
return "What do you mean? African or European?"
}
}
var airspeedVelocity: Double {
switch self {
case .african:
return 10.0
case .european:
return 9.9
case .unknown:
fatalError("You are thrown from the bridge of death!")
}
}
}
다른 값 유형과 마찬가지로, UnladenSwallow는 Bird
와 Flyable
프로토콜을 준수하므로 알맞은 프로퍼티를 정의해야 합니다. 또한, canFly에 대한 기본 구현을 가져옵니다.
Overriding Default Behavior
UnladenSwallow
타입은 Bird 프로토콜을 준수함으로써 자동적으로 canFly를 구현합니다. 그러나, 당신은 UnladenSwallow.unknown
의 경우 canFly에 대해 false
를 반환하기를 원합니다. 기본 구현을 재정의 할 수 있을까요? 네 그렇습니다.
extension UnladenSwallow {
var canFly: Bool {
return self != .unknown
}
}
.african
과 .european
은 canFly에 대해 true
를 반환할 것입니다. 테스트 해봅시다.
UnladenSwallow.unknown.canFly // false
UnladenSwallow.african.canFly // true
Penguin(name: "King Penguin").canFly // false
이 방법을 통해, 객체 지향 프로그래밍에서의 가상 메소드와 흡사하게 프로퍼티와 메소드를 재정의 할 수 있습니다.
Extending Protocols
표준 라이브러리에서 프로토콜을 활용하고 기본 동작을 정의할 수 있습니다.
Bird
프로토콜을 CustomStringConvertible
프로토콜에 부합하도록 수정합니다. :
protocol Bird: CustomStringConvertible {
CustomStringConvertible
을 준수하는 것은 description 프로퍼티가 필요하며 문자열처럼 동작할 필요가 있다는 것을 의미합니다. 그렇다면 이제 이 프로퍼티를 모든 Bird
타입에 추가해야 한다는 뜻일까요?
네 그렇습니다, 프로토콜 확장을 통해 쉬운 방법이 있습니다. 다음과 같은 코드를 추가합니다:
extension CustomStringConvertible where Self: Bird {
var description: String {
return canFly ? "I can fly" : "Guess I’ll just sit here :["
}
}
이 확장은 canFly 프로퍼티를 Bird
타입의 description 값으로 나타내도록 합니다.
확인하기 위해, 아래 코드를 추가합니다:
UnladenSwallow.african
“I can fly!”
가 assistant editor에 나타나는 것을 확인 할 수 있습니다. 하지만 더욱 주목할 만한 것은, 여러분은 단지 자신만의 프로토콜만 확장했다는 것입니다!
Effects on the Swift Standard Library
기능을 확장하고 커스텀하는데 프로토콜 확장이 왜 좋은 방법인지 살펴보았습니다. 스위프트 팀이 스위프트 표준 라이브러리의 작성 방법을 개선하기 위해 프로토콜을 어떻게 사용할 수 있었는지 당신을 놀라게 할 수도 있습니다.
다음 코드를 추가하세요:
let numbers = [10,20,30,40,50,60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()
let answer = reversedSlice.map { $0 * 10 }
print(answer)
이것은 꽤 간단하게 보일 것이고, 여러분은 심지어 프린트된 답을 추측할 수도 있을 것입니다. 놀라운 것은 관련 타입입니다. 예를 들면, slice
는 정수 배열이 아니라 ArraySlice<Int>
입니다. 이 특별한 wrapper 타입 은 원본 배열의 뷰 역할을 하며 빠르게 추가할 수 있는 값 비싼 메모리 할당을 방지합니다. 유사하게, reversedSlice 는 실제로 ReversedRandomAccessCollection<ArraySlice<Int>> 이며, 다시 원본 배열의 wrapper 타입의 뷰입니다.
다행스럽게도, 표준 라이브러리를 개발하는 천재들은 이 프로토콜을 준수하기 위해 Sequence 프로토콜과 모든 컬렉션 래퍼(수십 가지가 있음)에 대한 확장으로서 map
메소드를 정의했습니다. 이렇게 하면 ReversedRandomAccessCollection만큼이나 쉽게 배열 에서 Map을 호출할 수 있으며, 차이점을 알 수 없습니다.
Off to the Races
지금까지 여러가지 Bird
를 준수하는 타입을 정의했습니다. 이제 지금까지와는 다른 다음의 코드를 추가하십시오.
class Motorcycle {
init(name: String) {
self.name = name
speed = 200
}
var name: String
var speed: Double
}
지금까지 정의한 새나 날아다니는 것들과 아무 관련이 없는 클래스입니다. 펭귄 뿐만아니라 오토바이도 레이싱을 하고 싶습니다.
지금까지 했던 것을 모아봅시다.
Bringing it Together
protocol Racer {
var speed: Double { get } // speed is the only thing racers care about
}
extension FlappyBird: Racer {
var speed: Double {
return airspeedVelocity
}
}
extension SwiftBird: Racer {
var speed: Double {
return airspeedVelocity
}
}
extension Penguin: Racer {
var speed: Double {
return 42 // full waddle speed
}
}
extension UnladenSwallow: Racer {
var speed: Double {
return canFly ? airspeedVelocity : 0
}
}
extension Motorcycle: Racer {}
let racers: [Racer] =
[UnladenSwallow.african,
UnladenSwallow.european,
UnladenSwallow.unknown,
Penguin(name: "King Penguin"),
SwiftBird(version: 3.0),
FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
Motorcycle(name: "Giacomo")
]
이 코드에서는, 먼저 Racer
프로토콜을 정의하고 모든 타입이 이것을 준수하도록 합니다. Motorcycle와 같은 몇몇 타입은 간단하게 준수합니다. UnladenSwallow
와 같은 것들은 조금의 로직이 필요합니다. 결국 Racer
타입을 준수하는 다양한 브런치를 가지고 있습니다. 모든 타입이 준수하면, Racer 배열을 만듭니다.
Top Speed
이제 Racer의 최고 속도를 결정하는 함수를 작성해야합니다.
다음을 추가하십시오:
func topSpeed(of racers: [Racer]) -> Double {
return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0
}
topSpeed(of: racers) // 3000
이 함수는 표준 라이브러리의 max
를 사용하여 가장 빠른 속도의 Racer를 찾고 반환합니다. 빈 배열의 racers
를 전달하면 0을 반환합니다.
Looks like it’s Swift 3 FTW. As if it were ever in doubt! :]
Making it more generic
그래도 문제가 있습니다. racers
의 서브셋(slice)에 대한 최고 속도를 찾고 싶다고 가정해보십시오. 다음과 같은 코드를 추가하면 에러가 발생합니다.:
topSpeed(of: racers[1...3]) // ERROR
Swift는
CountableClosedRange
타입의 인덱스로 [Racer]
타입의 값을 서브스크립트 할 수 없다고 불평합니다. Slicing은 해당 wrapper 타입 중 하나를 반환합니다.
해결책은 구체적인 Array 대신에 일반적인 프로토콜을 작성하는 것입니다.
topSpeed(of:)
호출 앞에 다음을 추가하십시오.
func topSpeed<RacerType: Sequence>(of racers: RacerType) -> Double
where RacerType.Iterator.Element == Racer {
return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0
}
RacerType
은 이 함수의 제네릭 타입이며 Swift 표준 라이브러리의 Sequence
프로토콜을 준수하는 모든 타입이 될 수 있습니다. where절은 시퀀스의 요소 타입이 모두 Racer 타입을 준수해야 함을 지정합니다.
모든 Sequence
타입에는 Element 타입을 반복 할 수 있는 Iterator라는 관련 타입이 있습니다. 실제 메소드 본문은 이전과 거의 같습니다.
이 함수는 array slices를 포함한 모든 Sequence
타입에서 동작합니다.
topSpeed(of: racers[1...3]) // 42
Making it More Swifty
표준 라이브러리 플레이 북을 빌려서, Sequence
을 확장하여 topSpeed()
을 쉽게 작성할 수 있습니다. 다음 코드를 추가하십시오:
extension Sequence where Iterator.Element == Racer {
func topSpeed() -> Double {
return self.max(by: { $0.speed < $1.speed })?.speed ?? 0
}
}
racers.topSpeed() // 3000
racers[1...3].topSpeed() // 42
Protocol Comparators
Swift 3에서의 개선사항 중 하나는 연산자의 요구사항을 만드는 방법에 대한 것 입니다.
다음 코드를 추가하세요:
protocol Score {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
}
Score
프로토콜을 사용한다는 것은 모든 점수를 동일한 방식으로 처리하는 코드를 작성할 수 있음을 의미합니다. 그러나, RacingScore와 같은 다른 구체적인 타입을 사용하면 스타일 점수와 귀여움 점수와 혼동하지 않을 것이다.
당신은 점수가 비교 가능하길 원하기 때문에 누가 제일 점수가 높은지 말해야 합니다. Swift 3 이전에는, 이러한 프로토콜을 따르기 위해 전역 연산자 함수를 추가해야했습니다. 이제는 모델의 일부인 정적 메소드를 정의 할 수 있습니다.
Score
와 RacingScore
의 정의를 다음과 같이 바꾸면 됩니다.
protocol Score: Equatable, Comparable {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
static func ==(lhs: RacingScore, rhs: RacingScore) -> Bool {
return lhs.value == rhs.value
}
static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
return lhs.value < rhs.value
}
}
한 곳에서 RacingScore의 모든 로직을 캡슐화 했습니다. 이제 점수를 비교할 수 있으며 프로토콜 확장 기본 구현을 통하여 명시적으로 정의하지 않아도, 크거나 같은 연산자를 사용할 수도 있습니다.
RacingScore(value: 150) >= RacingScore(value: 130) // true