[WWDC18 Review #1] What’s New in Swift

Swift 4.2 and Swift 5.0

WWDC18에서도 예년과 다름없이 Swift가 업데이트 되었습니다. 그동안 1년 주기로 버전 번호가 1씩 증가했기 때문에 이번 메이저 업데이트 버전은 5.0일 것이라고 예상했지만 4.2로 발표되었습니다.

Swift 4.2는 개발 생산성 향상에 중점을 두고 업데이트 되었습니다.

  • 빌드 속도 향상
  • 언어의 효율성 향상 및 boilerplate 코드 제거
  • SDK 개선

Swift 5.0은 2019년 상반기에 발표할 예정이고 바이너리 호환성에 중점을 두고 개발하고 있습니다.

  • 향후 출시되는 컴파일러와 바이너리 호환성 유지
  • OS에 Swift Runtime 내장

특히, Swift Runtime이 OS에 내장되기 때문에 코드의 크기가 감소하고 앱 성능이나 메모리 사용량이 개선될 것으로 기대됩니다.

Source Compatibility

Xcode 10에는 하나의 스위프트 컴파일러가 내장되어 있고 Xcode 9과 마찬가지로 Multiple Language Compatibility Mode를 지원합니다. 기본 값은 Swift 4.2로 설정되고 Build Settings에서 Swift 3, Swift 4 버전을 선택할 수 있습니다. Xcode 10은 Swift 3 버전을 지원하는 마지막 버전이고, Apple은 Swift 4.2로 마이그레이션 하도록 권고하고 있습니다.

Speedup for Swift Debug Builds

Xcode 9에 비해 빌드 성능이 대폭 향상되었습니다. 프로젝트 규모와 CPU 코어 수에 따른 오차가 존재하지만 평균적으로 2배 이상 향상되었습니다.

이전 버전에서는 빌드 성능을 향상시키기 위해서 Debug Compilation Mode에서 Whole Module 옵션을 사용했습니다. Xcode 10에서는 새로운 Incremental 옵션을 사용할 수 있습니다.

Runtime Optimization

New Calling Convention: Guaranteed

새로운 “Guaranteed” 호출 규약이 도입되었습니다. 기존 “Owned” 호출 규약에서는 피호출자가 파라미터로 전달된 객체의 해제를 담당했습니다. 그래서 이 코드에서 x 상수에 저장된 객체는 foo(x:) 함수에서 해제됩니다. 이 코드에서는 파라미터로 전달된 객체가 필요한 시점에 사용되고 바로 해제되기 때문에 아무런 문제가 없습니다.

class X {
    let value = 123
}

func caller() {
    // + 1
    let x = X()
    foo(x: x)
}

func foo(x: X) {
    let y = x.value
    
    // release x
}

하지만 이 코드에서는 불필요한 retain과 release가 반복됩니다.

class X {
    let value = 123
}

func caller() {
    // + 1
    let x = X()
    // retain x
    foo(x) // release x in callee
    // retain x
    bar(x) // release x in callee
    // retain x
    baz(x) // release x in callee
}

func foo(_ x: X) {
    let y = x.value
}

func bar(_ x: X) {
    let y = x.value
}

func baz(_ x: X) {
    let y = x.value
}

새로운 “Guaranteed” 호출 규약에서는 피호출자가 객체를 해제하지 않습니다. 이전 코드와 달리 객체를 사용한 후에 정확히 한 번 해제됩니다. 그래서 불필요한 호출이 제거된만큼 코드 크기가 줄어들고 런타임 성능이 향상됩니다.

func caller() {
    // + 1
    let x = X()
    
    foo(x)
    bar(x)
    baz(x)
    
    // release x
}

Small String

Swift 4.2 문자열은 64bit 플랫폼에서 24바이트에서 16바이트로 크기가 변경되었습니다. 문자열을 15바이트에 직접 저장할 수 있다면 힙 메모리 생성없이 스택에 직접 저장합니다.

Reduced Code Size

Optimization Level에서 Optimize for Size를 선택하면 컴파일러가 생성하는 코드의 크기가 대략 10~30% 정도 감소합니다. 하지만 런타임 성능을 5% 정도 하락할 수 있습니다.

 

New Language Features

CaseIterable Protocol

열거형에 CaseIterable 프로토콜을 채용하면 모든 case가 포함된 allCases 속성이 자동으로 생성됩니다.

enum Gait: CaseIterable {
    case walk
    case trot
    case canter
    case gallop
    case jog
}

for gait in Gait.allCases {
    print(gait)
}

Conditional Conformance

Array, Dictionary, Optional에 저장된 요소가 Equtable, Hashable, Encodable, Decodable인 경우 Array, Dictionary, Optional 자체에도 해당 구현이 추가됩니다.

Synthesized Equatable and Hashable

Swift 4.0 버전에서는 커스텀 타입을 비교할 때 Equatable 프로토콜을 직접 구현해야 했습니다.

struct Restaurant {
   let name: String
   let hasTableService: Bool
   let kidFriendly: Bool
}

extension Restaurant: Equatable {
   static func ==(a: Restaurant, b: Restaurant) -> Bool {
      return a.name == b.name && a.hasTableService == b.hasTableService && a.kidFriendly == b.kidFriendly
   }
}


let r1 = Restaurant(name: "a", hasTableService: true, kidFriendly: true)
let r2 = Restaurant(name: "b", hasTableService: true, kidFriendly: false)

r1 == r2

Swift 4.1 버전에서는 모든 저장 속성이 Equatable인 경우 컴파일러가 Equatable 구현을 자동을 추가하도록 개선되었습니다.

struct Restaurant: Equatable {
   let name: String
   let hasTableService: Bool
   let kidFriendly: Bool
}

let r1 = Restaurant(name: "a", hasTableService: true, kidFriendly: true)
let r2 = Restaurant(name: "b", hasTableService: true, kidFriendly: false)

r1 == r2

하지만 Conditional Conformance의 경우에는 여전히 직접 구현해야 합니다.

enum Either<Left, Right> {
   case left(Left)
   case right(Right)
}

extension Either: Equatable where Left: Equatable, Right: Equatable {
   static func ==(a: Either<Left, Right>, b: Either<Left, Right>) -> Bool {
      switch (a, b) {
      case (.left(let x), .left(let y)): return x == y
      case (.right(let x), .right(let y)): return x == y
      default: return false
      }
   }
}

Swift 4.2에서는 이 부분까지 컴파일러가 자동으로 추가하도록 개선되었습니다.

enum Either<Left, Right> {
    case left(Left)
    case right(Right)
}

extension Either: Equatable where Left: Equatable, Right: Equatable { }

이 내용은 Hashable 프로토콜에도 동일하게 적용됩니다.

Hashable Enhancements

커스텀 타입에서 특정 필드만 비교하고 싶다면 이렇게 구현할 수 있습니다.

struct City {
   let name: String
   let state: String
   let population: Int
}

extension City: Equatable {
   static func ==(a: City, b: City) -> Bool {
      return a.name == b.name && a.state == b.state
   }
}

여기에 Hashable 구현을 추가한다면 name 속성과 state 속성의 해시를 계산하는 코드를 직접 구현해야 합니다. 해시 코드를 직접 구현하는 것은 어렵습니다. 직접 구현에 성공하더라도 성능상의 문제가 발생하거나 DoS(Denial-of-service) 공격에 취약해 질 수 있습니다.

extension City: Hashable {
   var hashValue: Int {
      // ???
   }
}

Swift 4.2에서는 Hashable 프로토콜에 새로운 hash(into:) 메소드가 추가되었습니다. name 속성과 state 속성의 해시를 계산하는 코드는 이렇게 구현할 수 있습니다. Hasher는 높은 품질과 고성능의 Hashing Algorithm을 제공합니다. 특히, Random Per-Process Seed를 통해 DoS 공격을 방지합니다.

struct City {
    let name: String
    let state: String
    let population: Int
}

extension City: Hashable {
    func hash(into hasher: inout Hasher) {
        name.hash(into: &hasher)
        state.hash(into: &hasher)
    }
}

Random Per-Process Seed는 앱 시작 시점에 자동으로 생성됩니다. 그래서 특정 항목의 해시는 이전 실행 시점의 해시와 달라집니다. 이런 특징으로 인해서 특정 해시값에 의존하는 코드가 의도한 대로 동작하지 않을수도 있습니다. 예를 들어 해시 값을 기준으로 정렬하는 코드가 있다면 정렬 순서가 달라질 수 있습니다.

Random Per-Process Seed는 Deterministic Hashing Environment Variable을 추가해서 비활성화 시킬 수 있습니다.

Random Number Generation

새로운 난수 생성 API가 추가되었습니다. 모든 숫자 자료형에는 random(in:) 메소드가 추가되었습니다. 더 이상 arc4random() 함수와 random() 함수를 사용할 필요가 없습니다.

let randomIntFrom0To10 = Int.random(in: 0 ..< 10)
let randomFloat = Float.random(in: 0 ..< 1)

컬렉션에는 randomElement() 메소드와 shuffled() 메소드가 추가되었습니다.

let greetings = ["hey", "hi", "hello", "hola"]
print(greetings.randomElement()!)

let randomlyOrderedGreetings = greetings.shuffled()
print(randomlyOrderedGreetings)

기본으로 제공되는 RNG 구현은 Apple 플랫폼과 Linux 플랫폼에서 모두 사용할 수 있습니다.

필요한 경우 RandomNumberGenerator 프로토콜을 활용해서 직접 RNG를 구현할 수 있습니다.

struct MersenneTwister: RandomNumberGenerator {
    // ...
}

var mt = MersenneTwister()

let randomIntFrom0To10 = Int.random(in: 0 ..< 10, using: &mt)
let randomFloat = Float.random(in: 0 ..< 1, using: &mt)

let greetings = ["hey", "hi", "hello", "hola"]
print(greetings.randomElement(using: &mt)!)

let randomlyOrderedGreetings = greetings.shuffled(using: &mt)
print(randomlyOrderedGreetings)

Checking Platform Conditions

플랫폼을 분기하는 코드가 개선되었습니다. Swift 4.1 버전까지는 iOS와 macOS 환경을 분기하기 위해서 이 코드를 사용했습니다.

#if os(iOS) || os(watchOS) || os(tvOS)
import UIKit
#else
import AppKit
#endif

Swift 4.2부터는 canImport라는 새로운 Build Configuration Directive를 사용할 수 있습니다.

#if canImport(UIKit)
import UIKit
#else
import AppKit
#endif

시뮬레이터 환경과 디바이스 환경을 분기하는 코드 역시 개선되었습니다. (Xcode 10 beta 버전에서는 아직 적용되지 않은 상태입니다)

// ~ Swift 4.1
#if os(iOS) || os(watchOS) || os(tvOS) && (arch(i386) || arch(x86_64))
   // Simulator
#else
   // Actual Device
#endif

// Swift 4.2
#if hasTargetEnvironment(simulator)
// Simulator
#else
// Actual Device
#endif

Implicitly Unwrapped Optionals

IUO가 구현 의도대로 동작하도록 새롭게 구현되었습니다. => https://swift.org/blog/iuo

Memory Exclusivity Checking

Overlapping Access 검출 성능이 향상되었습니다.

Swift 4.1에서는 호출자 path와 클로저에서 사용한 path로 인해서 Exclusivity Violation 오류가 발생합니다.

struct Path {
   var components: [String] = []
   
   mutating func withAppended(_ name: String, _ closure: () -> Void) {
      components.append(name)
      closure()
      components.removeLast()
   }
}

var path = Path(components: ["usr", "local"])
path.withAppended("bin") {
   print(path)
}

이 문제는 클로저에서 사용할 path를 명시적으로 지정해 주는 방식으로 해결할 수 있습니다.

struct Path {
   var components: [String] = []
   
   mutating func withAppended(_ name: String, _ closure: (Path) -> Void) {
      components.append(name)
      closure(self)
      components.removeLast()
   }
}

var path = Path(components: ["usr", "local"])
path.withAppended("bin") {
   print($0)
}

이어지는 코드에서도 유사한 문제가 발생하고 있지만 컴파일러는 오류로 판단하지 않습니다.

struct Path {
   var components: [String] = []
   
   mutating func withAppended<T>(_ name: String, _ closure: () -> T) -> T {
      components.append(name)
      let result = closure()
      components.removeLast()
      return result
   }
}
var path = Path(components: ["usr", "local"])
path.withAppended("bin") {
   print(path)
}

Swift 4.2에서는 Static Exclusivity Checking이 개선되었습니다. 그래서 이 코드에서 발생하는 문제점을 파악하고 오류를 출력합니다. 이 외에도 더 많은 오류를 검출할 수 있게 개선되었습니다.

그리고 Exclusivity Checking을 런타임에도 사용할 수 있고 릴리즈 빌드에서도 사용할 수 있게 되었습니다. 현재는 약간의 오버헤드가 발생하지만 차츰 개선될 예정입니다. 이 옵션은 Build Settings에서 선택할 수 있습니다.

이 글은 WWDC18 Session 401 What’s New in Swift의 내용을 요약한 글입니다. 글쓴이의 관심사가 아닌 일부 내용은 생략되어 있습니다.



댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다

*