Swift4의 키패스 표현

키패스는 어떤 객체의 프로퍼티 혹은 프로퍼티의 프로퍼티 체인에 대해서 그 이름을 통해서 값을 찾아나가는 표현을 말한다. Objective-C에서 키패스는 키패스 경로를 나타내는 문자열을 사용해서 특정 객체의 속성을 액세스하기 때문에 컴파일 타임이 아닌 런타임에 액세스해야 할 프로퍼티를 결정하는 동적 기능으로 키밸류코딩과 키밸류 옵저빙에 사용된다. Swift2까지는 Swift 내에 키패스에 대한 기능이 별도로 마련되지 않았고, NSObject의 value(forKey:)setValue(_:forKey:)를 사용하면서 문자열을 그대로 사용했다.

문자열을 통해서 키패스를 사용하는 것은 편리할 수는 있으나, 컴파일 타임에서 오타에 의해 존재하지 않는 키패스를 참조하는 것을 체크할 방법이 없어서 디버깅이 곤란한 부분이 있었다. 이 부분을 개선하기 위해 Swift3에서 #keyPath() 문법 표현이 추가되었는데, 이 문법은 코딩 시점에는 컴파일러의 도움을 받아 올바른 키패스를 확인할 수 있고, #keyPath() 표현을 통해 해당 키패스값을 문자열로 안전하게 변환할 수 있었다.

하지만 키패스를 문자열로 치환하는 이와 같은 방법은 Swift의 디자인 관점에서는 몇 가지 한계를 갖는다. 키패스 자체는 프로퍼티를 찾아가는 경로만을 정의하므로 타입 정보를 잃고 그 결과가 Any가 되어버린다든지, 파싱이 느리고 NSObject 기반의 클래스에서만 사용할 수 있었다. Swift4에서는 이러한 단점을 보완하고 클래스외의 모든 Swift 타입에서 키패스를 통해서 프로퍼티를 참조할 수 있는 범용적인 키패스 문법(과 키패스를 위한 코어 타입)이 추가되었다.

Swift4의 키패스 문법

Swift4의 키패스 문법은 단순히 백슬래시(\)로 시작하는 키패스 값을 말한다. Objective-C와 달리 self 대신에 타입 이름을 쓰거나, 타입이 분명한 경우, 타입 이름을 생략하고 바로 . 으로 시작하는 키패스를 사용할 수 있다. 다음은 Swift Evolution의 새로운 키패스 제안서에 실린 예제이다.

class Person {
  var name: String
  var friends: [Person] = []
  var bestFriend: Person? = nil
  init(name: String) {
    self.name = name
  }
}

var han = Person(name: "Han Solo")
var luke = Person(name: "Luke Skywalker")
luke.friends.append(han)

// 키패스 객체를 생성한다. 
let firstFriendsNameKeyPath = \Person.friends[0].name
// 생성한 키패스를 사용해서 프로퍼티를 액세스한다.
let firstFriend = luke[keyPath: firstFriendsNameKeyPath] // "Han Solo"

// 항상 . 으로 시작해야 한다. 이는 배열의 요소 참조시에도 마찬가지이다.
luke.friends[keyPath: \.[0].name]
luke.friends[keyPath: \[Person].[0].name] 

// 옵셔널 프로퍼티는 ?를 붙여서 액세스해야 한다.
let bestFriendsNameKeyPath = \Person.bestFriend?.name
let bestFriendsName = luke[Keypath: bestFriendsNameKeyPath] // nil

키 패스 타입

Swift4의 키패스는 KeyPath라는 타입에 의해서 관리된다. 이 타입은 하위 속성을 참조하기 위해서 다른 키패스를 이어 붙이는 것이 가능하고, 또한 루트 타입과 키패스가 가리키는 속성의 타입을 그 인자로 가질 수 있다. 이 말은 위 예제에서와 같은 표현으로 키패스를 생성하는 경우, luke의 타입인 Personname 속성의 타입인 String 에 대한 정보가 키패스 내부에 내제된다는 것이다. 따라서 위 예제에서 bestFriend 변수의 타입은 String이 라는 점을 컴파일러는 알 수 있다.