티스토리 뷰

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

BirdFlyable 프로토콜을 모두 준수하는 새로운 구조체 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라도 더이상 명시적으로 선언할 필요가 없다는 것입니다!


protocols-extend

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 타입의 뷰입니다.

secrets

다행스럽게도, 표준 라이브러리를 개발하는 천재들은 이 프로토콜을 준수하기 위해 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

이러한 모든 이질적인 타입들을 모두 레이싱을 위한 공통 프로토콜로 통합할 때입니다. 원래 모델의 정의를 다시 살펴보고 만지면서 이 작업을 수행 할 수 있습니다. 이것에 대한 멋진 용어는 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 타입을 준수하는 다양한 브런치를 가지고 있습니다. 모든 타입이 준수하면, 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


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
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 31
글 보관함