이 글은 Introducing Protocol-Oriented Programming in Swift 3 ( https://www.raywenderlich.com/148448/introducing-protocol-oriented-programming )의 번역입니다. (번역 허락을 받은 것은 아니지만, 도움이 필요하신 분들을 위해서 올려봅니다. 문제가 될 경우 바로 삭제하겠습니다. ㅡㅜ)
갱신 알림: 이 튜터리얼은 Niv Yahel에 의해서 Swift 3.0으로 업데이트 되었습니다. 원래 튜터리얼은 Erik Kerber( https://www.raywenderlich.com/u/kerber )에 의해서 작성되었습니다.
경주 게임을 개발하고 있다고 상상해보세요. 차, 오토바이, 심지어 비행기를 몰 수도 있습니다. 이런 타입의 애플리케이션을 만드는 것에 대한 일반 적인 접근 방법은 Object Oriented Design ( https://www.raywenderlich.com/81952/intro-object-oriented-design-swift-part-1 ) 을 사용하는 것입니다. Object Oriented Design 은 공통점을 공유하는 모든 객체로 상속되는 객체의 모든 내부 로직을 캡슐화합니다.
이런 설계 접근 방법은 효과가 있습니다. 하지만 몇가지 결점이 있습니다. 예를 들어, 게임 로직을 공유하기를 원할 수 있는 휘발유가 필요한 기계들, 배경에서 날아 다니는 새들 혹은 그 밖에 어떤 것을 만드는 기능을 추가한다면, 탈 것들의 기능적 컴포넌트들을 재사용 가능한 어떤것으로 분리할 좋은 방법이 없습니다. (객체 지향 방식에서는 객체로서 전혀 상관관계가 없는 기능들에 대해 로직을 재사용하여 공유하려고 할때 좋은 방법이 마땅치 않습니다.)
이런 시나리오가 protocol 들이 빛을 발하는 지점입니다. (protocol 은 객체로서 전혀 상관관계가 없는 타입들에 대해 로직을 공유할 수 있는 방법을 제공합니다.)
Swift는 항상 기존의 class, struct 그리고 enum 타입들을 보장하는 interface 를 protocol 들을 이용해서 지정할 수 있도록 해왔습니다. 일반적으로 protocol 들을 이용해서 기존의 class, struct 그리고 enum 타입들과 상호 작용합니다. Swift 2 에서 protocol 들을 확장하고 기본 구현들을 제공하는 방법이 소개되었습니다. Swift 3 에서는 연산자 준수(conformance)가 개선되었고, 표준 라이브러이에서 이러한 개선(연산자 준수(conformance) 향상)들이 새로운 숫자 protocol 들에 사용되었습니다.
protocol 은 매무 강력하고, 코드를 작성하는 방법을 변화시킬 수 있습니다. 이 튜터리얼에서는 protocol을 생성하고 사용하는 방법을 다룰 것입니다. 마찬가지로 코드를 더욱 확장성 있게 만들기 위해서 Protocol-Oriented Programming을 사용하는 방법도 다룰 것입니다.
Swift 팀이 Swift 표준 라이브러리 그 자체를 개선시키는데 어떻게 protocol 확장들을 사용할 수 있었는지 그리고 protocol들이 어떻게 여러분의 코드에 영향을 미치는지를 알게 될 겁니다.
새로운 playground 를 생성함으로써 시작하세요. Xcode에서 File\New\Playground .. 를 선택하세요. 그리고 playground를 SwiftProtocols 로 이름 정하세요. 이 튜터리얼의 모든 코드는 플랫폼에 상관없기 때문에 아무 플랫폼이나 선택할 수 있습니다. 저장할 곳을 정하기 위해서 Next 를 클릭하세요. 그리고 마지막으로 Create 를 클릭하세요.
새로운 playground 가 열리면, 다음 코드를 추가하세요.
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
이것은 name 과 canFly property들을 가지는 간단한 Bird 프로토콜을 정의합니다. 마찬가지로 airspeedVelocity 를 정의하는 Flyable 프로토콜도 정의합니다.
protocol 이전 세상에서는, Flyable 을 기본 클래스(base class)로 시작합니다. 그런 다음 Bird를 정의하기 위해서 객체 상속에 의지할 수 있을지도 모릅니다. 비행기들과 같은 다른 날아다니는 객체들도 마찬가지구요. 여기서 부터는 모든 것은 protocol로서 시작한다는 것을 주의하세요. protocol은 기본 클래스(base class)를 요구하지 않는 방법으로 함수의 개념을 캡슐화하는 것을 허용합니다.
다음에 실제 타입들을 정의하기 시작할 때, protocol이 전체 시스템을 어떻게 유연하게 만드는지 알 수 있을 겁니다.
playground의 끝에 다음의 struct 정의를 추가하세요.
struct FlappyBird: Bird, Flyable {
let name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
return 3 * flappyFrequency * flappyAmplitude
}
}
이 코드는 새로운 FlappyBird 라는 struct를 정의합니다. 이 struct는 Bird 와 Flyable 를 모두 준수(conform)합니다. 이 struct 의 airspeedVelocity 는 flappyFrequency 와 flappyAmplitude 의 함수로서 계산됩니다. FlappyBird는 canFly 에 대해 true 를 리턴합니다. :]
다음으로, 다음 두개의 struct 정의들을 playground의 끝에 추가하세요.
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로 만들지 않은 것은 좋은 생각입니다! protocol 들을 사용하면 기능 구성 요소를 정의하고 그 protocol들을 준수하는(conform) 관련된 객체를 가질 수 있습니다.
이미 여러 중복을 볼 수 있을 겁니다. 시스템에 Flyable 의 개념이 이미 있음에도 불구하고, Bird 의 모든 타입들이 (날 수 있는지 아닌지를 나타내는)canFly를 선언해야 합니다.(canFly가 계속 중복됩니다.)
protocol 확장할때, protocol의 기본 동작을 정의할 수 있습니다. Bird protocol 아래에 다음의 코드를 추가하세요.
extension Bird {
// Flyable birds can fly!
var canFly: Bool { return self is Flyable }
}
이 코드는 Bird에 대한 확장을 정의합니다. 이 확장은 타입이 Flyable 일 때마다 canFly 에 대한 기본 동작이 true 를 리턴하도록 합니다. 다른 말로, 어떤 Flyable 인 새도 더이상 canFly를 명시적으로 그렇게 선언할 필요가 없습니다!
let canFly = ... 를 FlappyBird, SwiftBird 그리고 Penguin struct 선언에서 삭제하세요. protocol 확장이 이제 요구사항(canFly)을 처리하기 때문에, playground 가 성공적으로 빌드가 되는 것을 볼 수 있을 겁니다.
protocol 확장들 과 기본 구현체(default implementation)들은 다른 언어의 base class 혹은 추상 클래스(abstract class( https://en.wikipedia.org/wiki/Abstract_type ))들을 사용하는 것과 비슷해 보일겁니다. 하지만, protocol 확장들 과 기본 구현체들은 Swift에서 몇가지 장점을 제공합니다.
-
타입들이 하나 이상의 protocol를 준수할(conform) 수 있기 때문에, 여러개의 protocol 들로 부터의 기본 동작을 가지고 장식될(decorated) 수 있습니다. 몇몇 언어들이 제공하는 다중 상속과는 다르게, protocol 확장은 어떤 추가적인 속성(state)도 도입하지 않습니다.
-
protocol 들은 class, struct 그리고 enum 들에도 적용가능합니다. 기본(base) class 들과 상속은 class 타입에 한정됩니다.
다른 말로, protocol 확장들은 class 들뿐만 아니라 value 타입에 대한 기본 동작을 정의하는 기능을 제공합니다.
struct 를 가지는 action 에서 이미 봤을 겁니다. 다음으로, 다음의 enum 정의들을 playground 의 끝에 추가하세요.
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 가 두 protocol 들을 준수하도록(conform) 적절한 property들을 정의하는 것입니다. UnladenSwallow 가 Bird 와 Flyable 를 준수하기(conform) 때문에, UnladenSwallow는 canFly에 대한 기본 구현체를 가집니다!
airspeedVelocity 를 포함하는 이 튜터리얼이 정말로 Monty Python 참조를 포함하지 않을 것이라고 생각했나요? :]
Bird protocol 준수(conforming)의 장점에 의해 UnladenSwallow 타입은 자동으로 canFly 구현을 가집니다. 하지만, UnladenSwallow.unknown 이 canFly 에 대해 false 를 리턴하기를 원합니다. 기본 구현체를 Override 하는 것이 가능할까요? 예, 이 코드들을 playground 끝에 추가 하세요.
extension UnladenSwallow {
var canFly: Bool {
return self != .unknown
}
}
이제 단지 .african 과 .european 만이 canFly 에 대해서 true 를 리턴할 겁니다. playground 끝에 다음의 코드를 추가하고 테스트 해보세요.
UnladenSwallow.unknown.canFly // false
UnladenSwallow.african.canFly // true
Penguin(name: "King Penguin").canFly // false
이 방법으로 객체지향 프로그래밍에서 virtual method 들을 Override 하는 것 처럼 property 들과 method 들을 Override 할 수 있습니다.
표준 라이브러리의 protocol 들을 활용하거나 기본 동작들을 정의할 수 있습니다.
CustomStringConvertible protocol 을 준수하도록(conform) Bird protocol 선언을 수정하세요.
protocol Bird: CustomStringConvertible {
CustomStringConvertible 을 준수한다는(comforming) 것은 String 처럼 동작하기 위해서 description property를 가질 필요가 있다는 것을 의미합니다. 그것은 현재와 미래의 모든 Bird 타입에 이 property를 추가 해야 한다는 것을 의미하나요?
물론 입니다, protocol 확장들을 이용하면 더 쉽게 할 수 있는 방법이 있습니다. Bird 정의 아래에 다음의 코드를 추가하세요.
extension CustomStringConvertible where Self: Bird {
var description: String {
return canFly ? "I can fly" : "Guess I’ll just sit here :["
}
}
이 확장은 canFly이 각각의 Bird 타입의 description 값을 나타내도록 할 겁니다. 그것을 시험해보기 위해서, 다음을 playground 끝에 추가하세요.
UnladenSwallow.african
"I can fly!"가 assistant 에디터에 나타난 것을 볼 수 있어야 합니다. 하지만 더 주목할만한 것은, 독자분들은 단지 자신만의 protocol을 확장했다는 것입니다!
protocol 확장이 가능성을 확장하고 커스터마이즈 하는데 얼마나 훌륭한 방법인지를 살펴봤습니다. 여러분을 놀라게 할 만한 것은 Swift 팀이 표준 라이브러리가 작성된 방법을 또한 개선하기 위해서 protocol들을 어떻게 사용할 수 있었는가 하는 겁니다.
playground 의 끝에 다음 코드를 추가하세요.
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 는 정수 배열(Array)이 아니고 ArraySlice 입니다. 이 특별한 wrapper type 은 원래 배열에 대한 view 처럼 동작하고 신속하게 추가 될 수 있는 비용이 많이 드는 메모리 할당을 피합니다. 유사하게 reversedSlice 는 실제로 다시 단지 원래 배열에 대한 wrapper type view 인 ReversedRanddomAccessCollection<ArraySlice> 입니다.
다행스럽게도, 표준 라이브러리를 개발하고 있는 천재들이 map 메소드를 Sequence protocol 에 대한 확장으로 정의했습니다. 그리고 모든 collection wrapper들이 이 protocol을 준수하도록(conform) 정의했습니다. 이것은 Array 에 대해 map을 호출하는 것을 ReversedRanddomAccessCollection 에 대해 호출하는 것 만큼이나 쉽고 별 차이점을 느낄 수 없게 만듭니다. 이 중요한 디자인 패턴을 잠시 빌려올 겁니다.
이제까지 여러분은 여러가지 Bird 준수(conforming) 타입을 정의했습니다. 이제 playground 끝에 완전히 다른 어떤 것을 추가하세요.
class Motorcycle {
init(name: String) {
self.name = name
speed = 200
}
var name: String
var speed: Double
}
이 클래스는 이제 까지 정의 해왔던 Birds 혹은 날아 다니는 것들(flying things)과 전혀 상관이 없습니다. 하지만, 여러분은 펭귄뿐 아니라 오토바이 경주에 참가하기를 원합니다. 모든 조각을 가져올 때입니다.
경주를 위해 공통 protocol과 이들 다른 타입들을 합칠 시간입니다. 뒤로 돌아가서 원래의 모델 정의들을 수정하지 않고 이것을 할 수 있습니다. 이것을 위한 근사한 용어는 retroactive modeling 입니다. 다음의 코드를 playground에 추가하세요.
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")
]
이 코드에서 protocol Racer 를 처음으로 정의합니다. 그리고 나서 모든 다른 타입들이 protocol Racer 를 준수하게(conform) 만듭니다. Motorcycle 같은 몇몇 타입들은 간단하게 protocol Racer 준수합니다(conform). UnladenSwallow 같은 다른 것들은 좀 더 많은 로직이 필요합니다. 마침내, 여러개의 Racer 준수(conforming) 타입들을 가지게 되었습니다.
모든 준수(conforming) 타입을 가지고 racer 들의 배열을 만듭니다.
이제 racer 들의 최고 속력을 결정하는 함수를 작성할 시간입니다. 이 코드를 playground 끝에 추가하세요.
func topSpeed(of racers: [Racer]) -> Double {
return racers.max(by: { $0.speed < $1.speed })?.speed ?? 0
}
topSpeed(of: racers) // 3000
이 함수는 가장 큰 속력을 가지는 racer를 찾고 리턴하기 위해서 표준 라이브러리 max를 사용합니다. 만약 사용자가 racer들에 대해 빈 배열을 넘긴다면 0 을 리턴합니다.
의심할 여지 없이 Swift 3 처럼 보입니다.:]
그런데 문제가 있습니다. racer들의 부분집합(slice)에 대한 최고 속력을 찾기를 원한다고 가정해봅시다. playground에 아래의 코드를 추가 하면 오류가 발생합겁니다.
topSpeed(of: racers[1...3]) // ERROR
Swift 는 CountableClosedRange 의 인덱스로 [Racer] 타입의 값을 단언(subscript)할 수 없다고 불평합니다. Slicing은 이런 wrapper 타입들의 하나를 리턴합니다.
해결책은 실제 Array 대신에 공통 protocol 에 대한 코드를 작성하는 것이다. 다음을 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 protocol을 준수하는(conform) 어떤 type이든 될 수 있습니다. where 절은 sequence의 element 타입이 Racer protocol을 준수해야(conform) 함을 나타냅니다. Sequence 타입들은 Iterator 라고 불리는 관계된 타입을 가집니다. Iterator 타입은 Element 타입들을 통해서 순환(loop)할 수 있습니다. 실제 메소드 본체(body)는 이전과 것과 거의 같습니다.
이 메소드는 array slice들을 포함하는 어떤 Sequence 타입에 대해서도 잘 동작합니다.
topSpeed(of: racers[1...3]) // 42
좀 더 잘 할 수 있습니다. 표준 라이브러리 play book 으로 부터 빌려와서, topSpeed()가 쉽게 발견될 수 있도록 Sequence 타입 그 자체를 확장할 수 있습니다. 다음의 코드를 playground 끝에 추가하세요.
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
이제 메소드가 찾기 쉽고, racer들의 sequence를 다룰 때도 그냥 적용하면 됩니다.
Swift 3의 protocol들에 대한 한 개선점은 연산자 요구사항을 생성하는 방법입니다. playground 끝에 다음의 코드를 추가하세요.
protocol Score {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
}
Score protocol을 가진다는 것은 같은 방법으로 모든 점수들을 처리할 수 있는 코드를 작성할 수 있다는 것을 의미합니다. 하지만, RacingScore 와 같은 다른 단단한(concrete) 타입들을 가짐으로써 이 점수들이 스타일 점수들이나 귀여움 점수들과 섞이지 않는다는 것을 확신할 수 있습니다. 컴파일러 감사합니다!
높은 점수가 누구인지 이야기 할 수 있도록 점수들이 비교가능하기를 원합니다. Swift 3 이전에는, 이런 protocol들을 준수하는(conform) global operator 함수들을 추가 해야 했었습니다. 지금은 모델의 일부로 이들 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 에 대한 모든 로직을 캡슐화 했습니다. 이제 점수들을 비교할 수 있게 되었습니다. 그리고, protocol extension default implementation들 의 마술로, 명시적으로 선언된 적 없는 greater-than-or-equal-to 와 같은 연산자들도 사용할 수 있게 되었습니다.
RacingScore(value: 150) >= RacingScore(value: 130) // true
이 튜터리얼의 모든 코드는 여기( https://koenig-media.raywenderlich.com/uploads/2016/11/SwiftProtocols.playground-1.zip )에서 다운로드 받을 수 있습니다.
간단한 protocol들을 작성하고 protocol 확장들을 이요해서 정의한 protocol을 확장함으로써 Protocol-Oriented 프로그래밍의 힘을 보았습니다. 기본 구현체(default implementations)들을 가지고, base class 매우 유사하게 하지만 struct와 enum들에도 적용할 수 있기 때문에 더 멋지게, 기존의 protocol들에 공통의 자동화된 동작을 추가할 수 있습니다.
게다가, protocol 확장들은 우리의 protocol들 뿐만 아니라, Swift 표준 라이브러리, Cocoa, Cocoa Touch, 혹은 third party 라이브러리의 protocol 들에도 기본 동작(default behavior)를 제공하고 확장할 수 있습니다.
계속해서 protocol들에 대해서 더 많은 것을 배우기 위해서는 애플 공식 문서( https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/Protocols.html )를 읽어보세요.
이면에 있는 이론을 더 자세히 들여다 보기 위해서, 애플의 개발자 포털에서 Protocol-Oriented Programming( https://developer.apple.com/videos/wwdc/2015/?id=408 )에 대한 훌륭한 WWDC 세션을 볼 수도 있습니다.
연산자 준수(conformance)에 대한 논리적 근거는 Swift evolution proposal( https://github.com/apple/swift-evolution/blob/master/proposals/0091-improving-operators-in-protocols.md )에서 찾을 수 있습니다.
Swift collection protocol 들에 대해서 더 배우고 싶고, 직접 만들는 방법을 배우고 싶을지 모릅니다.( https://www.raywenderlich.com/139591/building-custom-collection-swift )
마지막으로, "새로운" 프로그래밍 패러다임에 너무 과하게 흥분하거나 모든 것에 대해서 이것을 사용하려고 하기 쉽습니다. Chris Eidhof 의 재미있는 블로그( http://chris.eidhof.nl/post/protocol-oriented-programming/ )는 우리에게 만병통치약과 "단지 그래서" 모든 곳에 protocol을 적용하는 것에 대해서 주의해야 한다고 상기시켜줍니다.
질문이 있으세요? 포럼 토론 아래에 남겨 주세요!
이글을 읽어 주셔서 감사합니다.

