[Objective-C] Key-Value Coding (KVC)

키 벨류 코딩(key-value coding, KVC)은 문자열(키)를 이용해 객체의 프로퍼티에 접근(set or get)할 수 있게 해준다. 접근자(Accessor Methods)를 통하지 않고 프로퍼티 이름인 문자열을 통해 프로퍼티에 접근이 가능하므로, 객체간 의존성을 낮춰주고 이는 느슨한 구조의 소프트웨어 설계를 할 수 있게 한다.

예를 들어 name 프로퍼티는 [object setValue: @”NewName” forKey: @”name”] 같이 변경할 수 있으며 이는 [object setName :  @”NewName”] 과 동일하다.

KVC 를 구현하는 메소드는 NSKeyValueCoding Protocol 에 의해 정의되며 NSObject 는 이를 구현하고 있다.

KVC 가 문자열을 통해 객체 프로퍼티에 접근하는 내부 구현은 다음과 같다. (valueForKey : 메소드와 setValue: ForKey: 메소드의 상세 내부 원리는 차이가 있으나 큰 틀에서는 비슷하다. 자세한 내용은 Accessor Search Implementation Details 참고)

  1. key 값과 일치하는 프로퍼티를 찾는다.
  2. 프로퍼티가 없으면 key 값과 일치하는 인스턴스 변수명을 찾는다.
  3. 프로퍼티 또는 인스턴스 변수가 발견되면 발견된 프로퍼티 또는 인스턴스 변수를 적용한다.
  4. 일치하는 프로퍼티 또는 인스턴스 변수가 없으면 valueForUndefinedKey : 또는 setValue: ForUndefinedKey: 메시지를 호출한다. (기본적으로 이 메소드는 Exception 을 발생시키며, 필요에 따라 override를 통해 재정의 가능하다.)

위와 같은 내부 원리로 인해 KVC 는 접근자를 통한 프로퍼티 접근보다 속도가 약간 느리다. 따라서 KVC 는 소프트웨어 구조 측면에서 유연성이 필요한 경우에만 사용하는 것이 좋다.

KVC 는 KVO(Key-Value Observing), Cocoa Binding, Core Data, 어플리케이션의 AppleScript 적용의 기술적 기반이 된다.

KVC 호환 클래스 설계

Key-Value Observing, Key-Value Binding , Key-Value Scripting 같은 기술을 사용할 때 대부분 KVC 를 간접적으로 사용한다. 프로퍼티를 조사하거나 바인딩 할 때 모두 KVC 를 사용하기 때문이다. KVC 를 사용하려면 객체가 KVC 와 호환되게 해야 한다.

위에서 설명한 바와 같이 KVC는 인스턴스 변수에 대해 인트로스펙션을 사용해 접근한다. 따라서 인스턴스 변수는 추가적인 작업없이 KVC와 호환된다.

@property 를 이용할 경우 애플에서 권고하는 네이밍 룰을 사용해야 한다. @synthesize 를 이용하면 프로퍼티의 get/set 메소드를 자동으로 생성하기 때문에 상관 없지만 get/set 메소드를 직접 구현할 경우 룰을 따라야 한다.

일반적으로  get 메소드는 프로퍼티 이름을 get 메소드 이름 그대로 사용한다. Boolean 타입의 경우 프로퍼티 이름 앞에 is 를 붙일 수 있다.  set  메소드의 경우 프로퍼티 이름앞에 set 을 붙이고 프로퍼티 이름 첫글자는 대문자를 사용한다. (KVC Accessor Methods 참고)

KVC 는 non-object 타입 (scalar 와 struct) 도 지원하며 자동으로 NSNumber, NSValue 로 자동 wrapping 된다.

프로퍼티가 object 타입이 아닌 경우, nil 값 설정을 대비한 조치를 취할 필요도 있다. setNilValueForKey : 메소드는 프로퍼티에 nil 값이 설정될 때 호출되는데 이 메소드를 이용하면 Boolean 타입 같은 non-object 타입에 nil 값이 설정될 때 적절한 값으로 대체할 수 있다.

아래 예제 코드는 Boolean 타입인 hidden 프로퍼티에 nil 값 설정이 시도되면 ‘YES’ 값으로 초기화 하고, hidden 프로퍼티가 아닌 경우에는 nil 값을 설정하는 코드다.

-(void)setNilValueForKey:(NSString *)theKey{

    if([theKey isEqualToString:@"hidden"]){
        [self setValue:@"YES" forKey:@"hidden"];
    } else {
        [super setNilValueForKey:theKey];
    }

}

Collection 적용

KVC 는 프로퍼티 이름이 경로(path)에 결합되고 컬렉션에 적용될 때 힘을 발휘하여 코드를 간결하게 해준다.

NSObject 에는 -valueForKey: (NSString*) key, -setValue : (id) value forKey: (NSString*), -valueForKeyPath: (NSString*)path, 그리고  -setValue: (id)value forKeyPath: (NSString*)path 메소드가 정의돼 있다. key 는 하나의 프로퍼티 이름을 말하며, path 는 단순한 프로퍼티 이름이나 여러 개의 프로퍼티 이름, 연산의 복합적 경로를 말한다.

KVC 가 컬렉션에서 사용될 때 Collection Operators 를 같이 사용하면 유용한다. 컬렉션 연산자(collection operators)는 컬렉션에 포함된 객체들을 문자열을 통해서 다양한 연산이 가능하도록 한다. 기본적인 포멧은 아래와 같다. (자세한 내용은 Collection Operators 참고)

< from iOS developer library >

“@” 로 시작하는 컬렉션 연산자(Collection operator) 좌측에 있는 keyPathToCollection (Left key Path)은 연산자가 적용될 컬렉션의 경로를 의미한다. 컬렉션 연산자 우측에 있는 keyPathToProperty (Right key path)는 연산자가 사용하는 인자(프로퍼티)의 경로를 의미한다.

간단한 예시를 들어보자. 예시를 위한 컬렉션의 데이터는 아래 그림과 같다. Transaction 클래스는 3개의 프로퍼티(payee, amount, data)를 가지며, Transactions 컬렉션에 아래 그림과 같이 13개의 Transaction 객체가 포함되어 있다.

< from iOS developer library >

1. @count, @avg, @max, @min, @sum

위 연산자는 컬레션 특정 프로퍼티의 개수, 평균값, 최대값, 최소값, 그리고 합계를 의미한다. Transactions 컬렉션에서13개의  amount 프로퍼티 평균값은 다음과 같이 가져올 수 있다. (다른 연산자 사용법은 이와 비슷하다)

NSNumber *average = [transactions valueForKeyPath:@"@avg.amount"];

연산자 포멧에서의 설명처럼 연산자 ‘@avg’ 우측에 연산자가 사용할 인자로 ‘amount’ 라는 프로퍼티를 적어준다. 연산자의 대상 컬렉션은 transactions 로 이 경우에는 연산자 좌측에 적지 않고 valueForKeyPath 메시지 수신자로 명시되어 연산자의 대상으로 사용되었다. 코드의 결과로는 Transactions 컬렉션이 소유하고 있는  13개 Transaction 클래스의 amount 프로퍼티 평균값이 계산되어 반환된다. NSNumber 타입으로 wrapping되어 반환된다.

만약 valueForKeyPath 메시지의 수신자가 transactions 가 아니고, Transactions 와 Transactions 와는 완전히 타른 타입의 컬레션을 프로퍼티를 포함하는 또 다른 객체 – MySuperTransactions – 라고 한다면, 위 예시 중 메소드 호출부는 [mySuperTransactions valueForKeyPath:@”transactions.@avg.amount”] 와 같은 형태로 변경되어야 원하는 값을 얻을 수 있다. MySuperTransactions 가 포함하는 컬렉션 프로퍼티들 중 타겟으로 삼는 컬렉션 프로퍼티를 지정해줘야 한다.

2. @distinctUnionOfObjects, @unionOfObjects

컬렉션 내부 객체들의 배열들 반환하는 연산자도 존재한다. @distinctUnionOfObjects 는 프로퍼티 값의 중복을 제거한, 유일한 프로퍼티 값으로 이루어진 배열을 반환하며 @unionOfObjects 는 모든 프로퍼티가 포함된 배열을 반환한다.

NSArray *payees = [transactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

예시 컬렉션 데이터를 대상으로  위 코드를 돌린 결과는, 13개 Transaction 객체의 payee 프로퍼티 값들 중 중복된 값을 제외한 값, 즉  5개의 문자열( Car Loan, General Cable, Animal Hospital, Green Power, Mortgage )로 이루어진 배열이 반환된다.

@unionOfObjects 연산자를 사용한다면 모든 amount 값이 포함된 배열이 반환될 것이다.

KVC 를 컬렉션에 적용할 때 컬렉션이 포함하는 값들 중에 nil이 있으면 exception이 발생한다. 따라서 nil 이 들어갈 수밖에 없는 상황이면 nil의 객체 타입인 NSNull 을 이용해야 한다.

3. @distinctUnionOfArrays, @unionOfArrays, @distinctUnionOfSets

하나의 컬렉션 뿐만 아니라 여러 개의 컬렉션을 대상으로 연산도 가능하다.
예를 들어 위 13개 Transaction 객체를 가지는 Transactions 컬렉션 외에 같은 타입의 또 다른 Transactions 컬렉션이 존재하며, 이 두 개의 Transactions  컬렉션을 포함하는 arrayOfTransactions 컬렉션이 있다고 가정하자. 이 경우 두 개의 Transactions 컬렉션을 대상으로도 연산이 가능하다.

NSArray *payees = [arrayOfTransactions valueForKeyPath:@"@distinctUnionOfObjects.payee"];

위 연산의 결과는 두 개의 Transactions 이 포함한 모든 객체의 payee 프로퍼티 값을 대상으로 중복을 확인한 후, 중복되지 않은 payee값만이 포함된 배열을 반환한다. 만약 대상 컬렉션이 Set 타입인 경우 연산자 이름 중 Set 이 포함되는 연산자를 사용하면 된다.

NSArray 나 NSSet 유형이 아닌 커스텀 배열을 프로퍼티로 사용하여  KVC 와 호환되게 하려면 몇 개의 특별 메소드를 구현해야 한다. 자세한 내용은 Collection Accessor Patterns for To-Many Properties  참고하자.

* Reference