키밸류코딩(Key-Value Coding : KVC)은 Objective-C의 어찌보면 가장 중요한 디자인 패턴 중의 하나이다. 굉장히 편리한 Objective-C의 기능들 중 다수가 키밸류코딩에 기반하거나, 많은 부분을 의존한다. 특히 키밸류옵저빙, 코어데이터, 코코아바인딩 등 코딩의 수고스러움을 비약적으로 줄여주는 기술들이 키밸류코딩을 기반으로 하고 있다.
하지만 이에 대해서 자세한 내용을 다루고 있는 글들이 상당히 많음에도 많은 이들이 이 키밸류 코딩에 대해 잘못 이해하고 있다는 사실은 상당히 놀랍다. 이 글에서는 키밸류 코딩에 대한 오해와 실체에 대해 살펴보고, 키밸류 코딩을 이해하는 시도를 해 보고자 한다.
프로퍼티
원칙적으로 객체는 그 내부에 구현이나 정보들을 외부에서 알지 못하도록 숨긴다. 따라서 객체 내부의 변수나 값에 접근하기 위해서는 접근자라는게 필요하다. 보통 코코아 책에서는 다음과 같은 모양으로 클래스를 만든다.
#import
@interface Person : NSObject
{
NSString *firstName, *lastName;
}
-(NSString *)firstName;
-(NSString *)lastName;
-(void)setFirstName:(NSString *)newFirstName;
-(void)setLastName:(NSString *)newLastName;
@end
코드 1 ) Person의 인터페이스
#import "Person.h"
@implementation Person
-(id)init {
self = [super init];
firstName = @"";
lastName = @"";
return self;
}
-(NSString *)firstName
{
return firstName;
}
-(NSString *)lastName
{
return lastName;
}
-(void)setFirstName:(NSString *)newFirstName
{
firstName = [newFirstName copy ];
}
-(void)setLastName:(NSString *)newLastName
{
lastName = [newLastName copy];
}
@end
코드 2) Person의 구현부
이 Person 이라는 클래스는 firstName, lastName 이라는 이름을 구성하는 두 개의 문자열 변수를 인스턴스 변수로 가지고 있다. 그리고 객체 외부에서 이 이름들에 접근하기 위해 인스턴스 변수 1개당 2개씩의 메소드를 만들어주었다. 각각의 이름은 -firstName, -lastName, -setFirstName:, -setLastName: 으로 인스턴스 변수의 이름과 유사한 형태를 띄고 있다. 하지만 외부에서 액세스해야하는 정보의 양이 많아지면 이런 간단하고 단순한 접근자 메서드들을 계속해서 만들어주어야 한다는 사실은 게으른 우리로서는 슬프기 그지 없다. 그래서 Objective-C에서는 “외부에서 사용가능한 객체의 정보”라는 개념으로 프로퍼티라는 것을 마련해 두고 있다. 이는 마치 마법처럼 위의 코드를 비약적으로 간결하게 줄여준다.
#import
@interface Person : NSObject
{
NSString *firstName, *lastName;
}
@property (copy, nonatomic) NSString *firstName;
@property (copy, nonatomic) NSString *lastName;
@end
코드 3) 프로퍼티 구문을 이용한 Person의 인터페이스
#import "Person.h"
@implementation Person
@synthesize firstName;
@synthesize lastName;
-(id)init {
self = [super init];
firstName = @"";
lastName = @"";
return self;
}
코드 4) @systhesize 구문으로 노가다를 제거한 Person의 인터페이스
즉, @property~@synthesize 쌍으로 이루어진 구문으로 각각의 인스턴스 변수를 “이것은 프로퍼티다”라고 말해주면 컴파일러는 알아서 맨 위의 코드에서 노가다로 만들었던 getter, setter 메서드들을 자동으로 생성해준다. 이런 메서드들을 접근자(accessor)1라고 한다.
즉 프로퍼티는 그 자체로 실체가 있는 개념이라기보다는 객체 외부에서 그 객체를 살펴봤을 때 객체 내부에 이렇게 활용 가능한 정보들이 있다라는 명세서와 같은 개념이라고 볼 수 있다. 어차피 객체 내의 인스턴스 변수는 숨겨져 있으므로, 접근자메서드를 통해 액세스할 수 있는 정보들만 “프로퍼티”가 될 수 있고, @property~@synthesize 구문은 “이런 이런 것들이 프로퍼티로 존재한다”라는 것을 명시해서 컴파일러로 하여금 이에 대한 접근자를 자동으로 만들어주라고 하는 것이다.
심지어 프로퍼티는 반드시 인스턴스 변수일 필요도 없다. 예를 들어 맨 처음의 예에서 인스턴스변수는 두개 뿐이지만,
-(NSString *)fullName
{
return [NSString stringWithFormat:@"%@ %@",firstName,lastName];
}
이라는 메서드를 추가해준다면, Person의 외부에서는
NSString *hisFullName = [aPerson fullName];
과 같은 식으로 풀네임을 얻어올 수 있는데, 이 fullName 역시 프로퍼티가 된다!
@property (nonatomic, readonly) NSString *fullName;
이라고 인터페이스 파일에서 선언해주면 별도의 fullName을 위한 인스턴스 변수를 만들지 않아도, fullName 이라는 프로퍼티를 이용할 수 있는 것이다. 프로퍼티는 개념적으로 객체외부에서 정보를 얻는 방법일 뿐이다.
키밸류 코딩에 대한 오해
문제는 인터넷에서 찾아볼 수 있는 많은 글에서 이렇게 접근자 메서드를 자동으로 생성해주는 @property~@synthesize 구문을 키밸류 코딩이라고 설명하고 있는 경우가 너무 많다는 것이다. 분명히, 이는 키밸류 코딩이 아니고 “프로퍼티”라고 하는 Objective-C의 다른 기능을 의미하는 것이다.
키밸류 코딩에 관한 진실
키밸류코딩은 사실 별게 아니다. 그런데 별 게 아니기 때문에 이를 명확하게 설명하기가 힘들 뿐이다. 키-밸류 코딩은 말 그대로 키를 가지고 값에 접근한다는 의미이다. 실질적으로는 NSString 문자열의 내용을 가지고 객체 내부에 존재하는 값에 접근하게 된다.
따라서 @propery~@synthesize 구문이 자동으로 접근자를 생성해주는 것은 키밸류 코딩이 아니라고 명확하게 말할 수 있다. 하지만 진정으로 중요한 것은 이 구문이 생성해주는 접근자 메소드의 이름이 왜 하필 인스턴스 변수와 같은 이름을 갖느냐는 것이다. 실제로 이 구문을 사용하면 -firstName, -setFirstname: 과 같이 메소드가 만들어진다. 이름이 이렇게 되는 이유, 그것은 키밸류 코딩으로부터 시작하게 된다. 이 시점에서는 둘의 연관관계는 별로 없지만, 나중에 가면 이런 규칙은 정말 ‘위대해’지기 까지 한다.
valueForKey:, setValue:forKey:
키 밸류 코딩은 이 두 메소드를 사용해서 객체의 프로퍼티 혹은 인스턴스 변수에 접근한다. 여기서 ‘혹은’이라는 단어도 중요하다. 위에서 만든 Person 클래스의 인스턴스인 person에게 이름을 지어준다고 생각해보자.
Person *person = [[Person alloc] init]; person.firstName = @"KILDONG"; person.lastName = @"KIM";
키밸류 코딩에서는 접근자 메서드를 키 이름을 기준으로 만든다고 했다. 이 때 person.firstName 과 같이 구두점으로 구분하는 문법을 사용하면 이는 키밸류 코딩의 접근자 이름 규칙을 따른다고 간주하고 실제로는 [person setFirstName:@"KILDONG"]; 이라는 구문과 완전하게 동일하게 동작하게 된다. 다만, 이는 키밸류 코딩의 이름 규칙으로 얻을 수 있는 부가적인 장점이지, 키밸류 코딩의 핵심적인 내용은 아니다.
즉, 구두점을 통한 액세스는 키밸류 코딩에서 사용하는 이름 짓기 방식을 그대로 따르고 있기 때문일 뿐이고, 실제 해당 객체가 키밸류 코딩을 따르지 않고 있더라도 사용이 가능하며, 키밸류 코딩을 따른다고 구두점 구분법을 사용해야 할 이유또한 없다. (둘은 완전히 별개이다.)
실질적인 키밸류 코딩은 “문자열”을 통해 키에 접근하는 방식이다. 따라서 person의 이름 짓기는 이렇게도 가능하다.
[person setValue:@"KILDONG" forKey:@"firstName"]; [person setValue:@"KIM" forKey:@"KIM"];
그런 다음, 실제로 제대로 된 이름이 들어가 있는지 확인해볼 수도 있겠다.
NSLog(@"person's full name : %@ %@", person.firstName, person.lastName]);
여기까지는 별로 놀랍지도 않다. 다만 되려 ‘편리함’을 강조하는 키밸류 코딩이 사용하는 setValue:forKey: 를 사용하는 것이 더 번거롭지 않느냐는 이야기를 하는 것이 맞을 수도 있겠다.
이번에는 진짜 마법을 볼 차례이다. 위에서 작성한 코드3), 4)를 아래와 같이 대폭 간소화하자.
#import
@interface Person : NSObject
{
NSString *firstName, *lastName;
}
@end
코드 5) 프로퍼티 선언을 제거한 인터페이스
#import "Person.h"
@implementation Person
-(id)init{
self = [super init];
firstName = @"";
lastName = @"";
return self;
}
@end
코드 6) 프로퍼티의 합성을 제거한 구현부. 이제 Person의 인스턴스 변수는 완전히 숨겨진다.
이렇게 변경하더라도,
[person setValue:@"KILDONG" forKey:@"fistName"]; [person setValue:@"HONG" forKey:@"lastName"]; NSLog(@"person's name : %@ %@",[person valueForKey:@"fistName"], [person valueForKey:@"lastName"];
와 같이 시험해보면 에러없이 컴파일 되며, 심지어 결과 역시 정확하게 나온다. 이 Person 이라는 클래스는 내부에 무슨 변수가 있는지 알수도 없고, 접근자 메서드도 제공하지 않지만 키-밸류 코딩은 마치 마법처럼 person 객체에 값을 쓰고, 또 읽어왔다.
키밸류코딩, 마법의 비밀
어떤 NSObject의 하위 클래스가 (대부분의 코코아 클래스는 NSObejct의 하위클래스인데, 이는 엄청나게 다행인 것이다!) valueForKey: 메시지를 받게되면, 이 메소드는 다음과 같이 동작하게 된다. 예를 들어 키 이름이 myFavoriteMovie 라고 하자. (일부러 길게 씀) 즉 어떤 객체가 -valueForKey:@”myFavoriteMovie” 라는 메시지를 받았다면,
- 먼저 그 객체에 -myFavoriteMovie 라는 메소드가 있는지 살펴본다. 만약 존재한다면 객체는 [self myFavoriteMovie]라고 메시지를 보내고 그 결과값을 리턴해준다.
- 그런 메소드가 없다면 비슷한 다른 이름들을 찾아본다. -getMyFavoriteMovie, -isMyFavoriteMovie 라는 메소드를 찾아본다. 이런 이름이 있다면 그 메소드를 실행해서 나온 결과값을 리턴해준다. 그마저도 없다면 여러 다양한 가능성을 찾아본다. 예를 들어 -countOfMyFavoriteMovie, -objectInMyFavoriteMovieAtIndex:, -myFavoriteMovieAtIndexes 등등 키밸류 코딩의 이름 규칙이 적용되는 여러 메소드들을 찾아본다. 특히 이들은 배열이나 세트와 관련된 메소드들인데, 이런 메소드가 있다면 이 메소드 내에서 사용하는 배열의 프록시 객체를 리턴해준다.
- 메소드 중에서 일치하는 이름을 찾아내지 못한다면, 이번에는 인스턴스 변수를 찾는다. myFavoriteMovie, isMyFavoriteMovie, _myFavoriteMovie, _isMyFavoriteMovie 등의 인스턴스 변수를 찾는다. 있다면 그 인스턴스 변수에 저장된 “객체”를 리턴한다. (-valueForKey: 는 id형을 반환하므로 객체를 받게 된다.)
- 그마저도 없다면 그 키는 이 객체에 없다는 것이다. 객체는 [self valueForUndefinedKey:@"myFavoriteMovie"]; 를 호출하고 그 메소드는 (따로 오버라이드하지 않았다면) 예외를 발생시킨다.
이와 같이 다소 복잡한 경로를 거쳐서 값을 액세스하는 방법이다. 따라서 직접적인 액세스에 비해서는 느리지만, 활용하기에 따라서는 상당히 강력하게 활용할 수 있다. 특히 객체들의 정보를 표로 보여주는 경우에 테이블의 칼럼명 (identifier)을 객체의 키 이름으로 정한다면 키밸류 코딩을 사용하여 여러 케이스를 나눌 필요 없이 한 방에 이를 처리할 수도 있다.
실제로 valueForKey: 메소드를 사용할 경우는 그리 많지가 않을수도 있다. 그러나 키밸류 코딩이 따르고 있는 이름 만들기 규칙만큼은 키밸류 코딩에 의존하는 수많은 기술들에서 활용되고 있으며, 구두점을 통한 액세스와 같이 (person.firstname) 이와 무관해 보이는 부분들에 있어서 엄청 많은 편의성을 제공해주게 된다. 특히 모델의 복잡도나 규모가 커지면 키패스를 사용해 많은 양의 코드를 절약하게 되고, 성능의 향상도 도모할 수 있다.
키밸류 코딩 따르기
키밸류 코딩은 valueForKey;, setVale:forKey: 의 메소드를 사용하는 것보다, 키밸류 코딩에서 정하는 이름 규칙에 맞게 프로퍼티나 메소드를 만들어 사용하는 것에서 더 큰 잇점을 취할 수 있다. 상당히 유용한 키밸류 옵저빙 매커니즘은 특정 객체의 일부 정보가 변경되는 것을 원격으로 알아차리는 방법인데, 이 역시 키 밸류 옵저빙 이름 규칙을 따를 때 효율적으로 이용할 수 있게 된다. 따라서 이런 이름 규칙을 습관화 해 놓으면 별도의 노력이나 주의를 기울이지 않고도 많은 장점들을 쉽게 얻을 수 있다는 점에서 알아두는 것이 좋을 것 같다.
- 접근자라는 말은 딱딱한 느낌이 들어서 좋은 번역이라 할 수는 없는데, 마땅한 단어가 생각이 나지 않는다. ↩