Key-Value-Coding
많은 프로그래밍 언어에서 캡슐화된 객체의 프로퍼티 값을 얻거나 수정하기 위해서는 객체에 정의한 getter
, setter
메서드를 통해 접근할 것이다. 아마 스위프트도 getter
, setter
메서드 혹은 연산 프로퍼티 등 비슷한 방식으로 접근할 것이다. 이러한 객체의 프로퍼티에 직접적으로 접근과는 다르게 Objective-C와 스위프트는 Key-Value-Coding(KVC)를 통해 객체의 프로퍼티에 간접적으로 접근할 수 있다. Objective-C에서 KVC 방식을 사용하기 위해서는 객체가 NSKeyValueCoding 프로포콜을 채택해야 한다. 다행히도 프로토콜 메서드의 기본적인 구현이 되어 있기 때문에 따로 구현해야 하는 것은 없다.
Identifying an Object’s Properties with Keys and Key Paths
KVC를 채택한 객체에 key와 keyPath를 통해 객체의 프로퍼티에 간접적으로 접근할 수 있다. 아래는 공식문서에 있는 Objective-C로 짜인 BankAccount 객체이다. 그렇다면 key와 keyPath란 무엇인가?
@interface BankAccount : NSObject
@property (nonatomic) NSNumber* currentBalance; // An attribute
@property (nonatomic) Person* owner; // A to-one relation
@property (nonatomic) NSArray< Transaction* >* transactions; // A to-many relation
@end
- Objective-C로 짜인 BankAccount 객체 -
Key
key는 프로퍼티를 식별하는 문자열로 일반적으로 프로퍼티의 이름과 동일하다. key는 아래와 같이 몇 가지 컨벤션 규칙이 있다.
- ASCII 인코딩을 사용한다.
- 공백을 포함할 수 없다.
- 보통 소문자로 시작한다.(단, URL 같은 프로퍼티는 예외)
공식문서에 있는 코드에 따르면 setter 메서드를 사용하지 않고 [myAccount setValue:@(100.0) forKey:@"currentBalance"];
처럼 key로 접근하는 것을 말한다. (myAccount는 BankAccount 객체의 인스턴스) 컨벤션 규칙에 의해 키인 currentBalance
는 프로퍼티의 이름과 동일하다.
KeyPath
keyPath도 마찬가지로 문자열이지만 점(.)으로 연결된 키(key) 문자열이다. myAcount.owner.address.street
처럼 말이다. (Person, Address객체 모두 KVC를 준수한다는 가정) keyPath는 상위 객체의 키를 기준으로 시작하여 하위 객체의 프로퍼티(street)의 키까지 계층적으로 접근하는 특징이 있다. 공식 문서에 있는 코드에 따르면 [myAccount setValue:@(100.0) valueForKeyPath:@"owner.address.street"];
으로 접근할 수 있다.
KeyPath in Swift
스위프트는 KeyPath를 사용하여 KVC를 사용할 수 있다. \
(Back Slash)를 사용한 후 뒤에 베이스 타입을 적어둔 뒤 .
(Dot)를 이용해 연쇄적으로 키에 접근하면 된다. 예를들어 \BankAccount.ownder.address.street
이다.
스위프트의 KeyPath는 값에 대한 참조가 아니라 프로퍼티에 대한 참조이다. KeyPath는 아래처럼 몇 가지 종류가 있다.
KeyPath<Root, Value>
- only readWritableKeyPath<Root, Value>
- read/write for value type(ex. struct)ReferenceWritableKeyPath<Root, Value>
- read/write only for reference types(ex. class)PartialKeyPath<Root>
AnyKeyPath
WritableKeyPath
struct Beans {
var type: String
}
struct Coffee {
var name: String
var price: Int
var beans: Beans
}
var bean = Beans(type: "kenya")
var coffee = Coffee(name: "Kenya Coffee", price: 1000, beans: bean)
let name = \Coffee.name
print(coffee.name) // Keyna Coffee
coffee[keyPath: name] = "케냐 커피"
print(coffee.name) // 케냐 커피
Coffee는 구조체로 값 타입이면서 name 프로퍼티는 var 변수이므로 \Coffee.name
은 WritableKeyPath이다.
ReferenceWritableKeyPath
class Beans {
var type: String
init(type: String) {
self.type = type
}
}
class Coffee {
var name: String
var price: Int
var beans: Beans
init(name: String, price: Int, beans: Beans) {
self.name = name
self.price = price
self.beans = beans
}
}
var bean = Beans(type: "kenya")
var coffee = Coffee(name: "Kenya Coffee", price: 1000, beans: bean)
var name = \Coffee.name
print(coffee.name) // Keyna Coffee
coffee[keyPath: name] = "케냐 커피"
print(coffee.name) // 케냐 커피
참조 타입인 클래스로 바꿀 경우 \Coffee.name
은 ReferenceWritableKeyPath으로 변한다.
KeyPath
class Coffee {
let name: String
var price: Int
var beans: Beans
init(name: String, price: Int, beans: Beans) {
self.name = name
self.price = price
self.beans = beans
}
}
var name = \Coffee.name
name이 let 변수로 바뀔 경우 객체의 타입에 상관없이 프로퍼티의 값은 불변하기 때문에 프로퍼티는 오직 읽기만 가능하다. 따라서 \Coffee.name
은 KeyPath로 변한다.
장점
KVC은 반복되는 코드를 줄일 수 있고, 특정 프로퍼티에 접근하는 접근 메서드를 줄여주기 때문에 클래스를 재사용 가능하게 만든다는 장점이 있다. 또한 프로그램의 실행 중에서 원하는 프로퍼티의 값을 가져올 수 있도록 설계할 수 있는 장점이 있다.
Reference
- Key-Value Coding Programming Guide
- KEY VALUE CODING AND KEY VALUE OBSERVING
- KVC와 KVO의 활용 및 목적 질문글의 야곰님 댓글