Swift3에서 프로토콜 지향 프로그래밍 도입하기(Introducing Protocol-Oriented Programming in Swift3)
Swift/Tip 2017. 4. 25. 16:51원문 : https://www.raywenderlich.com/148448/introducing-protocol-oriented-programming
여러분이 레이싱 게임을 개발한다고 상상해 보세요. 차를 운전할수 있고, 오토바이를 달리거나 심지어는 비행기로 날수도 있습니다. 이러한 타입의 앱을 만드는 일반적인 방법은 객체 지향 설계(object oriented design)를 사용하는 것이며, 공통점을 갖는 모든 곳에서 상속받는 객체 내부의 모든 로직을 캡슐화 합니다.
이러한 설계 방식은 동작하지만, 몇가지 단점(drawbacks)이 있습니다. 예를 들어, 가스가 필요한 기계를 만들거나, 백그라운드에서 날고 있는 새, 또는 게임 로직을 공유하길 원하는 능력을 추가한다면, 탈것(vehicles)의 구성요소들을 분리해서 재사용하는 것은 좋은 방법이 아닙니다.
이 시나리오에서 프로토콜(protocols)의 진가를 볼수 있습니다.
Swift는 항상 프로토콜(protocols)을 사용해서 기존 class, struct, enum
타입에 대한 인터페이스를 보장 해줍니다. 이를 통해서 일반적인 상호작용을 할 수 있습니다. Swift 2에서 프로토콜을 확장하고 기본 구현을 제공하는 방법을 도입하였습니다. 마침내, Swift 3에서 표준 라이브러리에서 연산자 적합성(conformance)을 향샹시키고 표준 라이브러리의 새로운 수치(numeric) 프로토콜에 이러한 개선 사항을 사용합니다.
프로토콜은 매우 강력하고 코드 작성 방법을 바꿀수 있습니다. 이 튜토리얼에서, 프로토콜을 만들고 사용하는 방법을 탐구하고, 코드를 더 확장가능하게 만들기 위해 프로토콜 지향 프로그래밍(protocol-oriented programming) 패턴을 사용할 것입니다.
또한, Swift팀이 Swift 표준 라이브러리 자체를 향상시키기 위해서 프로토콜 확장을 어떻게 사용했는지 보고, 작성한 코드에 어떤 영향이 미치는지 볼수 있습니다.
시작하기(Getting Started)
새로운 플레이그라운드(playground)를 만드는 것으로 시작합니다. Xcode에서, File\New\Playground…를 선택하고 플레이그라운드 이름을 SwiftProtocols로 합니다. 여러분은 어떤 플랫폼을 선택할 수 있으며, 이 튜토리얼의 모든 코드는 플랫폼에 구애받지 않습니다. 저장위치를 선택하기 위해 Next를 클릭하고, 마지막으로 Create을 클릭합니다.
새 플레이그라운드가 열리면, 다음 코드를 추가하세요.
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
airspeedVelocity
를 정의한 Flyable
프로토콜 뿐만아니라 name
과 canFly
프로포티를 가진 간단한 프로토콜 Bird
를 정의합니다.
프로토콜 이전에는, Flyable
를 기본클래스로 시작했을수 있고 Bird
와 비행기와 같이 날아다니는 것을 정의하기 위해 객체 상속에 의존했습니다. 여기서 주의할 점은, 모든것(everything)은 프로토콜로 시작합니다!! 이렇게 하면 기본 클래스가 필요없는 방식으로 기능적인 개념을 캡슐화 할 수 있습니다.
다음에 실제 타입 정의를 시작할때, 전체 시스템을 보다 유연하게 만드는 방법을 알게 될것입니다.
프로토콜을 준수하는 타입 정의하기(Defining Protocol-Conforming Types)
플레이그라운드 아래에 다음 struct
정의를 추가하세요.
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
의 함수로 계산됩니다. flappy는 canFlay
에 대해 true
를 반환 합니다. :]
다음으로, 플레이그라운드 아래에 다음에오는 두개의 구조체 정의를 추가하세요.
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
이지만, 날수는 없습니다. - 상속으로 접근하는 것을 사용하지 않는것이 좋고, 상속은 모든 새들이 날수 있도록 만듭니다!! 프로토콜을 사용하면 기능 구성요소를 정의할 수 있고 모든 관련된 객체는 프로토콜을 준수해야합니다.
여러분은 이미 몇가지 중복된 것을 볼 수 있습니다. 이미 시스템에 Flyable라는 개념이 있지만, 모든 Bird
타입은 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
새는 더이상 명시적으로 선언할 필요가 없습니다!!
FlappyBird, SwiftBird, Penguin
구조체 선언에서 let canFly = ...
를 삭제합니다. 프로토콜 확장으로 요구사항을 처리한 후에, 플레이그라운드가 성공적으로 빌드된 것을 볼수 있습니다.
왜 기본클래스가 아닌가?(Why Not Base Classes?)
프로토콜 확장과 기본 구현은 다른 언어에서 기본클래스를 사용하거나 추상클래스(abstract classes)를 사용하는 것과 비슷하게 보일수 있지만, Swift에서는 몇가지 장점을 가지고 있습니다.
- 타입이 하나 이상의 프로토콜을 준수할수 있기때문에, 여러 프로토콜의 기본 동작을 표현(decorated) 할수 있습니다. 일부 언어에서 지원되는 클래스의 다중 상속과는 다르게, 프로토콜 확장은 추가적인 상태를 도입하지 않습니다.
- 프로토콜을 클래스나 구조체, 열거형에서 사용(adopted) 할 수 있습니다. 기본 클래스와 상속은 클래스 전용(restricted)입니다.
다른 말로, 프로토콜 확장은 클래스뿐만 아니라 value
타입에 대한 기본 동작을 정의하는 기능을 제공합니다.
이미 구조체를 사용하는 것을 보았습니다. 다음으로, 플레이그라운드의 끝에 다음에 오는 열거형(enum) 정의를 추가하세요.
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
가 두개의 프로토콜을 준수하도록 올바른 프로퍼티를 정의하기만 하면 됩니다.
airspeedVelocity
와 관련된 이 튜토리얼이 Monty Python 레퍼런스를 포함하지 않을것이라고 생각하나요? :]
기본 동작 재정의하기(Ovrriding Default Behavior)
UnladenSwllow
타입은 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
이런식으로, 객체 지향 프로그래밍에서 가상 메소드를 사용하는 것처럼 메소드와 프로퍼티를 재정의(override) 할 수 있습니다.
프로토콜 확장하기(Extending Protocols)
표준라이브러리로 부터 프로토콜을 활용하고 기본(default) 동작을 정의할 수 있습니다.
CustomStringConvertible
프로토콜을 준수하도록 Bird
프로토콜 선언을 수정합니다.
protocol Bird: CustomStringConvertible {
CustomStringConvertible
를 준수하는 것은 타입에 문자열처럼 동작하는 description
프로퍼티가 필요하다는 의미입니다.
현재와 미래의 모든(every) Bird
타입에 이 프로퍼티를 추가해야 한다는 의미인가요?
물론, 프로토콜 확장을 사용하면 더 쉽습니다. 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
보조 편집기(assitant editor)에 I can fly
라은 메시지가 나타납니다. 특히나, 여러분은 여러분의 프로토콜을 확장했습니다!
Swift 표준 라이브러이에서의 효과(Effects on the Swift Standard Library)
프로토콜을 확장이 어떻게 사용자 정의 하고 기능을 확장할 수 있는 훌륭한 방법인지 살펴보았습니다. Swift팀이 Swift 표준 라이브러리 작성을 개선하기 위해 어떻게 프로토콜을 사용할 수 있었는지를 알면 놀랄 것입니다.
플레이그라운드 끝에 다음 코드를 추가 하세요.
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)
이것은 매우 직설적(straightforward)으로 보이고, 출력된 답을 추측할수 있습니다. 놀랄만한것은 연관된 타입입니다. 예를 들어, slice
는 정수형 Array
가 아니지만 ArraySlice<Int>
입니다. 이 특별한 wrapper type 동작은 원래 배열의 뷰 역할을 하고 비싼 메모리 할당을 피하고 빨리 추가할 수 있습니다. 마찬가지로, reversedSlice
은 실제적으로 원래 배열로 감싼 타입(wrapper type)인ReversedRandomAccessCollection<ArraySlice<Int>>
입니다.
운이 좋게도, 표준 라이브러리를 개발하는 천재가 이 프로토콜을 준수하기 위해 map
메소드를 Sequence
프로토콜과 모든 컬렉션 래퍼(수십 가지가 있음)의 확장으로 정의하였습니다. 이렇게 하면 ReversedRandomAccessCollection
만큼이나 쉽게 Array
에서 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)
경주(racing)에 대해 이러한 다른 타입 모두를 공통 프로토콜로 통일할 때입니다. 원래의 모델 정의를 다시 수정할 수 있습니다. 이것에 대한 멋진(fancy) 용어는 소급 모델링(retroactive Modeling) 입니다. 플레이그라운드에 다음을 추가하세요.
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
타입을 준수해야 합니다.
모든 타입이 준수하고나면, 레이서(racers)의 배열을 만듭니다.
최고 속도(Top Speed)
이제 레이서의 최고 속도를 결정하는 함수를 작성할 시간입니다. 플레이그라운드의 끝에 이것을 추가하세요.
func topSpeed(of racers: [Racer]) -> Double {
return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0
}
topSpeed(of: racers) // 3000
이 함수는 레이서의 가장 빠른 속도를 찾아 반환하는 표준 라이브러리 max
를 사용합니다. racers
에 대해 빈 배열을 전달하면 0
을 반환합니다.
Swift 3 FTW 처럼 보입니다. 마치 의심하는 것처럼! :]
더 일반적으로 만들기(Making it more generic)
이것은 문제가 있습니다. 레이서(racers)
의 하위집단(slice)에서 최고 속도를 찾는다고 가정해 봅시다. 이것을 플레이그라운드에 추가하면 오류가 납니다.
topSpeed(of: racers[1...3])
Swift는 CountableClosedRange
타입의 인덱스로 [Racer]
타입의 값을 서브스크립트(subscript)로 사용할수 없다는 불평을 합니다. Slicing 은 래퍼 타입중 하나를 반환합니다.
해결책은 실제 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
은 함수에 대한 제네릭(generic) 타입이고 Swift 표준라이브러리의 Sequence
프로토콜을 준수하는 모든 타입이 될수 있습니다. where
절은 sequence
요소(element) 타입이 Racer
프로토콜을 준수해야 하는 것을 지정합니다. 모든 Sequence
타입은 Element
타입을 반복할수 있는 Iterator
와 연관된 타입을 가지고 있습니다. 실제 메소드 본문은 이전과는 거의 같습니다.
배열 조각(slices)를 포함한 모든 Sequence
타입에서 동작합니다.
topSpeed(of: racers[1...3]) // 42
더 Swifty하게 만들기(Making it More Swifty)
좀 더 나아질 수 있습니다. 표준 라이브러리( 에서, topSpeed()
을 쉽게 사용할수 있도록, Sequence
타입을 확장할수 있습니다. 플레이그라운드 끝에 다음을 추가하세요.
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
이제 이 메소드를 쉽게 사용할수 있지만 racers
의 sequences를 처리할때만 적용됩니다.(자동완성)
프로토콜 비교(Protocol Comparators)
Swift 3에서 프로토콜의 한 가지 개선사항은 연산자 요구사항을 만드는 방법입니다.
플레이그라운드 아래에 다음을 추가하세요.
protocol Score {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
}
Score
프로토콜을 사용한다는 것은 모든 점수(scores)를 동일한 방식으로 처리하는 코드를 작성할수 있다는 것을 의미합니다. 하지만, RacingScore
처럼 다른 구체적인 타입을 가짐으로써 스타일 점수(style scores)나 귀여움 점수(cuteness scores)로 이 점수들을 함께(mix up) 사용하지 않아야 합니다. 컴파일러에 감사!
점수가 비교되는 것을 원하므로 누가 높은 점수인지 말할수 있습니다. Swift 3 이전에는, 이 프로토콜을 준수하기 위해 전역 연산자 함수가 필요했었습니다. 이제는 모델의 일부만 정적(static) 메소드를 정의 할수 있습니다. 지금 바로 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
에 대한 모든 로직을 캡슐화했습니다. 이제 기본 구현을 프로토콜 확장의 마법으로, 점수를 비교할수 있고, 명시적으로 정의하지 않은 더 크거나 같은(greater-than-or-equal-to) 연산자를 사용할 수 있습니다.
RacingScore(value: 150) >= RacingScore(value: 130) // true
여기에서 어디로 가야 하나요?(Where To Go From Here?)
이 튜토리얼의 모든 코드로 완성된 플레이그라운드를 다운로드 받을수 있습니다. 다운로드
프로토콜 확장을 사용해서 간단한 프로토콜을 만들고 확장하는 프로토콜 지향 프로그래밍(protocol-oriented programming)의 힘을 보았습니다. 기본 구현으로, 기존 프로토콜에 공통적이고 자동 동작을 줄수 있으며, 기본 클래스와 많이 비슷하지만, 구조체와 열거형에도 적용할수 있기 때문에 더 좋습니다.
추가적으로, 프로토콜 확장은 자신의 프로토콜을 확장하는데 사용할 수 없지만, Swift 표준 라이브러리, Cocoa, Cocoa Touch 또는 타사(third party) 라이브러리의 프로토콜에 기본 동작을 제공하고 확장 할 수 있습니다.
프로토콜에 관해서 더 자세해 배우려면, 공식 애플 문서(offical Apple documentation)를 읽어야 합니다.
모든 이론에 대해 더 깊이 보려면, 애플 개발자 포털에 있는 프로토콜 지향 프로그래밍(Protocol Oriented Programming)에 대한 훌륭한 WWDC 세션을 볼 수 있습니다.
연산자 적합성에 대한 이론적인 근거는 Swift 발전 제안(Swift evolution proposal) 에서 찾을 수 있습니다. Swift 콜렉션 프로토콜에 관해 더 배우고 자신만의 만드는 방법을 배우고(learn how to build your own) 싶어 할수 있습니다.
마지막으로, 새로운(new)
프로그래밍 패러다임과 마찬가지로, 열광하기 쉽고 모든 것(all the things)에서 사용합니다. 흥미로운 Chris Edihof가 작성한 블로그 게시글(blog post by Chris Eidhof)은 우리에게 묘책(silver bullet)을 조심해야 하고 그냥(just because)
어디에서나 프로토콜을 사용해야 한다는 것을 상기시켜 줍니다.
'Swift > Tip' 카테고리의 다른 글
What’s New in Swift 4.1? (2) | 2018.04.03 |
---|---|
Swift 4에서 JSON 분석하기(Parsing JSON in Swift 4) (0) | 2017.07.11 |
Swift에서 상태 모델링하기(Modelling state in Swift) (0) | 2017.07.11 |
Swift Tutorial: An Introduction to the MVVM Design Pattern (1) | 2017.06.27 |
What’s New in Swift 4? (0) | 2017.06.27 |
Swift에서 사용자 정의 연산자 오버로딩하기(Overloading Custom Operators in Swift) (0) | 2017.05.01 |
Swift에서 사용자 정의 컬렉션 만들기(Building a Custom Collection in Swift) (0) | 2017.04.27 |
Bond Tutorial: Bindings in Swift (0) | 2017.04.20 |