Swift 4.1의 변경점

얼마전 Xcode의 업데이트가 있었고 Swift는 이제 4.0에서 4.1로 업데이트되었다. Swift 3 -> 4의 변경도 상당히 많은 개선과 변경이 있었는데, 4.1에서 새로 도입되는 기능들 중에서 소소하다고 넘기기에는 제법 굵직한 것들이 좀 있어서 소개해본다.

Hashable 및 Equatable의 향상

Equtable은 Swift에서 == 연산자를 적용할 수 있는 값의 성질이다. 기본적인 Swift의 데이터 타입들은 이 프로토콜을 만족하고 있다. 이전 버전까지는 우리가 커스텀 타입을 디자인할 때, Equtable을 따르도록 하고 싶다면, 모든 커스텀 타입에 대해서 == 연산자 함수를 일일이 정해주어야 했다. 예를 들어 다음과 같은 Person이라는 데이터 타입을 정의한다고 하자.

struct Person {
  var firstName: String
  var lastName: String
  var age: Int
}

Person이라는 타입은 직관적으로 Equtable을 정의할 수 있다고 보여진다. 이름과 성, 나이가 같은 Person 객체 값은 서로 같은 것이라 볼 수 있는 것이다. 따라서 다음과 같이 이 타입이 Equatable을 만족할 수 있게 만들 수 있다.

extension Person : Equatable {
  static func == (lhs: Person, rhs: Person) -> Bool {
    if lhs.firstName == rhs.firstName,
       lhs.lastName == rhs.lastName,
       lhs.age == rhs.age 
    { return true }
    return false 
  }
}

다만 모든 커스텀 타입에 대해서 == 연산자를 구현하는 것은, 분명 어렵지 않은 일이기는 하나 그것이 쉽거나 간단한 것을 의미하지는 않는다. 왜냐하면 프로퍼티가 많은 타입일수록 == 연산자의 구현이 매우 번거롭고 성가신 일이 될 것이기 때문이다.

하지만 Swift 4.1에서는 struct의 모든 프로퍼티가 Equatable을 따른다면, 이들에 의존하는 새로운 struct 타입의 == 연산자는 자동으로 생성될 수 있게 되었다. 따라서 Swift 4.1부터는 다음과 같이 Person 타입이 Equatable 하다고만 명시하면 된다.

extension Person: Equatable {  }

Hashable 프로토콜의 경우에도 자동 생성이 가능하다. Hashable 의 경우에는 hashValue 라는 Int 타입의 프로퍼티를 만들어야 하는데 역시 Swift 기본 타입값들은 그 자체의 hashValue를 내장하고 있다. 통상 특정 인스턴스의 해시는 그 내부의 구별해야하는 값들의 해시를 XOR로 조합해서 만든다. 예를 들어 Person의 경우에 Swift 4.0 이하에서는 다음과 같이 해시 값을 정의해주어야 Set을 만들거나 사전의 키로 사용할 수 있다.

extension Person : Hashable {
  var hashValue: Int {
    return firstName.hashValue ^ lastName.hashValue ^ age.hashValue &* 1231412
  }
}
// &*는 오버플로우를 허용하는 곱셈 연산이다. 
// 이를 통해서 해시 값이 Int의 범위를 넘어서더라도 동작할 수 있게 한다. 

Swift 4.1에서는 특정 인스턴스의 모든 프로퍼티가 Hashable이라면 이와 같은 동작을 디폴트로 합성해낼 수 있게 하기 때문에 구현부 없이 (물론 별도의 구현을 만들고 싶다면 그렇게 해도 된다.) 프로토콜 명만 지정해서 적용할 수 있다.

프로토콜의 조건부 적용

조건부 적용은 제네릭 타입에서 타입 파라미터가 특정 조건을 만족하는 경우에 해당 제네릭 타입이 프로토콜을 따르도록 한다는 것이다. 다음과 같이 임의의 프로토콜을 하나 생각해보자.

protocol Greetable {
  var fullName: String
  func greet()
} 

우리는 커스텀 타입인 Person이 이 프로토콜을 따르게 만드는 것에는 별 어려움을 느끼지 못할 것이다.

extension Person: Greetable {
  var fullName: String {
    return "\(firstName) \(lastName)"
  }
  func greet() {
    print("Hello, I'm \(fullName).")
  }
}

그런데 Person의 배열인 Array<Person>Greetable을 따르도록 하고 싶다면? 이전까지의 Swift에서는 제네릭 타입의 타입 파라미터가 특정한 조건을 만족할 때에만 확장은 가능했으나, 그 확장이 프로토콜 준수가 되기는 어려웠다.1 하지만 Swift 4.1에서는 다음과 같이 특정한 조건일 때에만 제네릭 타입이 어떤 프로토콜을 준수하는 것이 가능해졌다.

 

// 아래 코드는 Swift 4.0 이하에서는 Array 타입의 확장이
// 상속절을 가질 수 없다며 컴파일 되지 않는다. 
extension Array: Greetable where Element: Greetable {
  var fullName: Strig { 
    return self.map{ $0.fullName }.joined(separator: ", ")
  }
  func greet() {
    forEach{ $0.greet() }
  }
}

배열과 옵셔널

Swift에서 가장 흔히 쓰이는 제네릭은 배열과 옵셔널이다. 이 두 타입은 이미 그 원소의 타입이 Equatable인 경우에 별다른 확장 없이 == 연산자를 통해 비교가 가능하긴 했다.

// 옵셔널 값 비교
let a: Int? = 8
let b: Int? = 4 + 4
if a == b {
  print("two optionals are same")
}

// 배열 비교
let x = [8]
let y = [8]
if x == y {
  print("two arrays are same")
}

하지만 그럼에도 불구하고 옵셔녈의 배열은 이게 적용되지 않았다. (왜냐하면 Array에 대한 ==는 Swift의 기본 타입들에 대해서만 따로 오버로드가 되어 있었기 때문이다.) 하지만 Swift 4.1에서는 위 두 가지 기능이 조합되어 우리가 아무런 추가적인 코드를 부여하지 않아도 이것이 동작하게 된다.

let a: Int? = 8
let b: Int? = 4 + 4
if [a] == [b] {
  print("ok")
}

재귀적인 프로토콜 허용

A라는 프로토콜을 정의할 때, 프로토콜에서는 프로퍼티 하나를 가져야하는데, 이 프로퍼티의 타입이 구체적인 struct나 class가 아니라 프로토콜일 수 있을 것이다. 그런데 Swift 4 까지는 A라는 프로토콜 내에서는 타입이 A인 프로퍼티를 선언할 수 없었다. Swift 4.1에서는 이 제한을 없앴다. 따라서 다음과 같은 프로토콜을 정의할 수 있다.

protocol Employee {
  var manager: Employee? { get set }
}

참고로 이러한 프로토콜은 struct 보다는 class에 한정하여 적용하여야 할 것이다. struct의 경우에 타입 인스턴스를 위한 저장 공간을 할당해야 하는데, 이렇게 재귀적인 프로토콜을 사용하는 경우 메모리 할당의 무한루프에 빠질 수 있기 때문이다.

그외의 소소한 변화와 5.0

그외에 Swift 4.1 에서는 배열 및 옵셔널의 flatMap() 메소드를 새로 이름 붙인 compactMap() 이 추가되었다. 이름이 완전히 바뀐 건 아니고 새 이름이 추가되고 이전 flatMap()은 deprecated되었다고 경고가 나올 것이다.

이후 5.0에서 포함될 기능을 잠깐 언급하면 우선 ABI 안정화가 있다. ABI 안정화란 API가 변경되더라도 컴파일된 바이너리의 인터페이스가 가급적 유지될 수 있게 하는 것이다. 현재는 ABI가 안정화되지 않았기 때문에 Swift 프로젝트를 빌드하면 라이브러리들이 함께 패키징되어 들어가게 된다. Swift 버전이 변경된 경우, 라이브러리와 맞물리는 부분이 변경되면서 기존 버전의 Swift에서 컴파일된 바이너리가 올바르게 동작하지 못하기 때문이다. ABI 안정화가 이루어지면 Swift 버전에 상관없이 일단 컴파일된 바이너리는 라이브러리의 버전이 달라도 호환이 가능해진다. 그러면 각 앱마다 라이브러리를 포함할 필요 없이 시스템이 한 벌의 라이브러리만 내장하고 있으면 되기에 빌드 속도도 빨라지고, 바이너리의 크기도 크게 줄어들 수 있게 된다. 또, 시스템이 업데이트되면서 Swift 버전이 올라가도 이전 버전의 Swift에서 컴파일한 바이너리를 재 컴파일할 필요없이 계속 사용할 수 있게 된다.

그외에 Swift 5.0에서는 @dynamicMemberLookup 이라는 변경자가 클래스/구조체 선언에 포함될 수 있다. 이는 마치 파이썬 처럼 객체의 애트리뷰트를 런타임에 동적으로 액세스할 수 있게 해주는 기능이 될 것인데, (양날의 검이 되겠지만) 흥미로운 부분이라고 생각된다.


  1. 물론 Swift4.0 이전에서도 [Int] 타입이나 [String] 타입은 Int, String이 그러했던 것처럼 ==로 비교가 가능하다. 하지만 이것은 제네릭 타입인 배열의 일부가 Equatable이라기 보다는 == 연산자를 해당 구체적 타입에 맞추어 오버로드해서 제공했기 때문이다.