프로퍼티
프로퍼티에 대해 들어가기 전에 접근제어에 대하여
이것은 오브젝트가 가지는 인스턴스 변수로의 접근에 관한 구현 사항을 간략화 하게 해주는 기능이다.
C언어적인 자유도를 프로그래머에게 제공해 주고 싶은 Objective-C는 역시 그쪽에서는 자유 였다. 일단 인스턴스 변경에 @public、@protected、@private라고 하는 접근 수식자를 부가하는 것은 가능 하다. 그러나 @protected와 @private를 붙였다고 해도 컴파일 타임에 경고가 나오는 정도로 빌드도 나오고 실행도 문제없이 되버린다. 이게 Objective-C다운 방식이라 할수있다.... 라고 긍정적으로 어떻게 받아볼려고 하는 것도 역시 한계가 있다. 이런거라면 처음부터 그냥 없었던게 나았을 것이다.
@interface MyObject : NSObject
{
@private
// private 인스턴스 변수를 선언
int var;
}
@end
int main(int argc, const char* argv[])
{
MyObject* object;
object = [[MyObject alloc] init];
// 경고는 나오지만 빌드하는건 문제없다.
object->var = 100;
// 인스턴스 변수에 값이 잘 대입 되어있음
NSLog(@"var is %d", object->var);
return 0;
}
게다가 흉악한 지시자인
@defs라고 하는 컴파일러 지시자도 있다. 이걸 사용하면 클래스를 C언어의
구조체 처럼 접근하는게 가능하게 된다. 이미 오브젝트도 C언어적인 변수의 집합체로 밖에 보여질수
밖에 없어 보인다. 이런건 역시 Objective-C 2.0 에서는 deprecated로 된것같다
// MyObject클레스에 구조체같이 접근 가능하게끔 함
struct my_object {
@defs(MyObject)
};
int main(int argc, const char* argv[])
{
MyObject* object;
object = [[MyObject alloc] init];
// my_object구조체에 형변환해 사용
((struct my_object*)object)->var = 100;
NSLog(@"var is %d", ((struct my_object*)object)->var);
return 0;
}
이처럼 Objective-C에는 인스턴스 변수에 접근 하려고 생각하면 이런 방법을 사용해 접근 되어버린다.
반대로 접근을 금지시키는 것은 거의 불가능하지 않나싶다.
그럼 이것들의 기능을 적극 적으로 사용해 프로그래밍 해볼까 하고 한다면 그렇게 하는건 좀 아니라고 본다. 경험이 어느정도 쌓인 프로그래머라면 캡슐화를 높이기 위해 접근함수를 사용하는 것 하고 속도를 내기 위해 직접접근 인스턴스 변수에 접근 하는 것하고 분리해 구별할수 있을것이다. 어느쪽을 사용할 것 인가는 프로그래머의 재량에 달려 있다.
Objective-C의 언어사양을 보고있으면 "자유와 책임"이라는 말이 머리속에 떠오른다. 언어적으로 무엇을 금지시킨다는 것은 Objective-C에는 별로 없다. 오히려 적극적으로 내부구조를 접근하기 위한 문법과 API를 제공해 주고있다. 나머지는 그것을 사용하는 프로그래머의 책임인것이다. 위대한 자유에 대한 대가로, 질서있는 프로그램을 만드는 책임도 강요하는 것이다.
말이 좀 장대하게 되어버린 감이 있지만 어쨌든 이런 상황에서 나오게 된게 프로퍼티다.
프로퍼티의 목적
우선 프로퍼티의 목적을 명확히 해두자. 프로퍼티는 「클래스에 엑세스함수를 간단히 추가한다」를 위해 도입된 기능이다.
여기서 주의했으면 하는 것이 프로퍼티를 사용하면 그게 바로 엑세스함수를 호출하는게 된다 라는 것이다. 전 강좌의 Objective-C에서는 거의 자유스럽게 인스턴스 변수의 접근가능 하다는 점을 소개 했다. 프로퍼티에서는 이런 방식은 사용하지 않고 적절한 함수를 제공하게 된다. 이 방식의 장점은 안전하다는 점이다. 특히 Objective-C의 특유의 retain과 release를 동반한 오브젝트 관리의 문제점도 동시에 해결할수가 있다. (가비지 콜렉션을 사용하지 않는 경우)단점은 함수를 호출하는 것이므로 아무래도 퍼포먼스 면에서는 조금 부하가 생긴다는것 일것이다. 이런 장정과 단점을 파악한 다음 프로퍼티를 사용할것인가 말것인가를 결정 해줬으면 한다.
@interface에서의 프로퍼티
그럼 프로퍼티를 실제로 사용해보자 어떤 클래스에 프로퍼티를 추가하는 경우 @interface부 와 @implementation부의 양쪽에 손을 볼 필요가있다.
우선 @interface에서의 선언이다. 예를들어 MyObject라고하는 클레스에 NSString형의 name이라고 하는 프로퍼티를 추가해보자.
리스트1
@interface MyObject : NSObject
{
}
@property (retain) NSString* name;
@end
「@property」으로 시작하는 줄이 프로퍼티의 선언이다. @property뒤줄 괄호안에 속성을 기술하고 어떤 형(type)인지가 다음이고 마지막으로 프로퍼티 이름을 선언한다.
@property (속성) type이름 프로퍼티이름;
속성은 이 프로퍼티를 위해 제공되는 엑세스함수의 종류를 결정하게 된다. 이것에 대해서는 다음 강좌에서 상세하게 설명하겠다.
이것만 으로도 별 문제는 없지만 전형적인 사용법 으로는 프로퍼티는 관련된 인스턴스 변수를 위해 추가 되어진다는 것이다. @interface의 안에
프로퍼티를 사용해 접근하게 되는 인스턴스 변수도 추가해 두자
리스트2
@interface MyObject : NSObject
{
// 프로퍼티와 관련되어질 인스턴스변수
NSString* name;
}
@property (retain) NSString* name;
@end
여기에스는 프로퍼티의 이름과 인스턴스변수의 이름을 같게 해 두었다. 이렇게 해두면 구현할때 조금 편하게 할수있다. 그러나 이름이 같지 않아도 문제될것은 없다.
이걸로 프로퍼티의 형과 이름을 선언했다.
@implementation에서의 프로퍼티
다음으로 @implementation부다 여기에서는 프로퍼티의 엑세스함수의 실제 구현에 대해 선언하게된다. 프로퍼티의 장점은 엑세스함수를 자동으로 작성해 준다는 것이다. 이장점을 최대로 살릴려면 @synthesize을 사용한다. 다음처럼 작성해 나가면 된다.
리스트3
@implementation MyObject
@synthesize name;
@end
이걸로 완료다 @synthesize지시자에 프로퍼티 이름을 지정하면 엑세스함수를 자동으로 「생성」해준다. 같은이름의 인스턴스 변수인 name을 위해 getter와setter을 추가해준다.
프로퍼티 이름과 인스턴스 변수명이 같은 경우는 이걸로 충분하다. 이름이 다른 경우는 @synthesize에서 인스턴스 변수명을 명시적으로 지정 할 필요가 있다. 예를들면 인스턴스 변수명이 _name라고 해보자 이런경우는 다음과 같다.
리스트4
@implementation MyObject
@synthesize name = _name;
@end
프로퍼티 이름의 다음에 인스턴스 변수명을 지정하면 된다. 이걸로 프로퍼티를 임의의 인스턴수 변수에 관련시킬수 있다.
또 자동으로 함수생성 기능을 사용 안하고 수동으로 액세스함수를구현하는 것도 가능하다. 이런경우 name과 setName:라고하는 함수를 자기가 작성하면된다.
리스트5
@implementation MyObject
- (NSString*)name
{
...
}
- (void)setName:(NSString*)name
{
....
}
@end
가령 @synthesize을 쓰지않고 수동으로 액세스함수도 구현해 놓지않으면 컴파일 타임에 경고가 나오게 된다.
또 Objective-C다운 이야기 이지만 컴파일 타임에 엑세스함수가 존재하진 않지만 런타임에 동적으로 그것들이 추가되는 상황도 있을것이다. 이런 경우를 위해 @dynamic이라는 지시자도 준비되어있다.
리스트6
@implementation MyObject
@dynamic name;
@end
프로퍼티를 @dynamic으로 선언하면 액세스함수는 만들어지지도 않고 컴파일 타임에도 구현 되어있는지의 검사도 행하지 않는다. 실행하게 되면 처음으로 관련된 엑세스함수가 있는지 없는지 검사 하게 된다.
실제로 소스코드를 써보면 @synthesize을 사용하면 비교적 정중하게 에러 라던가 경고를 내준다. 관련된 인스턴스변수의 검사 라던가 속성이 적절한가 어떤가 등을 판단해 준다. 그러나 @dynamic을 사용하면 동적으로 확인하는 것이 되므로 컴파일타임의 검사는 거의 건너뛰게 된다. 이것은 좀 엉망진창 인것 같다는 느낌도 들지만 Objective-C다운 이라고 말하면 대충 말이 될것이다.
이게 프로퍼티의 선언이된다.
도트 연산자
인스턴스의 프로퍼티에 접근하기 위해 이것도 새로 도입된 도트 연산자를 사용한다. 도트 연산자는 C언어의 구조체에서 사용되고있다. 내부의 속성에 접근하는것으로 감각적으로 봤을때 이것과 비슷할것이다.
예를들면 name이라고 하는 프로퍼티를 가진 Person이라고 하는 클래스를 생각해보자 이 프로퍼티에 도트 연산자를 사용해 접근하는 것은 다음과 같은 코드가 된다.
리스트1
Person* person = [[Person alloc] init];
NSString* name = person.name;
person.name = @"Jobs";
이걸로 프로퍼티를 얻고 설정할수가 있다.
다만 표기하는것은 도트 연산자로 하지만 실제로는 함수를 불러내 처리한다는 것에 주의해 주길 바란다. 인스턴스 변수에 직접적으로 접근하는것이 아니다. 따라서 다음의 소스 코드는 리스트1과 같은 코드가 된다.
리스트2
Person* person = [[Person alloc] init];
NSString* name = [person name];
[person setName:@"Jobs"];
name에의 접근은 name함수와 setName: 함수 콜과 같다는 말이 된다. 프로퍼티 이름으로 부터 함수명으로의 변환규칙은 키값 코딩의 그것과 같다.
도트연산자
도트연산자는 실체는 함수를 불러내는 것이지만 컴파일타임의 처리가 다르다. 그부분을 봐보자.
우선 함수콜을 생각해보자. Objective-C의 경우 함수를 불러낼때 그 함수가 정말 존재하는지 아닌지의 체크는 런타임에 체크한다 라는것이 원칙이다. 따라서 id형의 오브젝트에 대응하는 함수의 호출은 특히체크도 하지않고 모두 컴파일은 된다. 오브젝트에 자료형이 지정되있는 경우에도 경고는 나오지만 결국 컴파일은 되버리고 만다.
예를들면 좀전의 Person클래스에 lastName이라고 하는 함수가 정의되어 있지않아도 이 함수를 호출하고 있는 소스코드는 문제없이 컴파일이 된다. 물론 그 상태로 실행하면 예외가 발생해 프로그램은 종료 되고 말아버리지만.
리스트3
id person = [[Person alloc] init];
// 존재하지 않는 함수를 호출
// 컴파일은 된다.
NSString* lastName = [person lastName];
이것에 대해 도트연산자는 좀더 엄격한 체크를 하도록 됬다. 오브젝트에 도트연산자를 적용 하면 그 자료형이 체크되도록 된다. 존재하지 않는 프로퍼티가 지정되어 있으면 컴파일이 되지않는다. 애시당초초 id형은 오브젝트에 도트연산자를 적용하는 것이 불가능하는 것이다. 도트연산자를 사용하고 싶은 경우 반드시 형변환을 하지 않으면안된다.
리스트4
id person = [[Person alloc] init];
// id형의 프로퍼티에 접근한다.
// 에러가 난다.
NSString* name = person.name;
// 존재하지 않는 프로퍼티에 접근 한다.
// 에러가 난다.
NSString* lastName = ((Person*)person).lastName;
이것을 가지고 프로퍼티는 「컴파일 타임의 정적인 자료형체크를 도입했다」라는 것이 가능하게 됬다. Objective-C와 같은 동적인 언어는 그 약점이 컴파일타임의 검사가 느슨하다고 거론 되어지고 있다. 프로퍼티의 처리는 정적인 성향의 개념을 도입하고 있다.
가장 동적인 「느슨함」이 Objective-C의 특징인 것이 사실이다. 프로퍼티를 사용하는 경우 늘 오브젝트의 자료형을 의식하고 또 그것을 소스코드에 형변환하는 방식으로 사용하지 않으면 안된다. 실제로 포로퍼티을 사용한 프로그래밍을 실행한 경우 id형을 오브젝트에 대해서 프로퍼티를 적용하고 싶은 경우가 많다. 예를들어 NSArray부터 꺼낸 오브젝트등에 이와같은 경우 형변환 하던가 자료형을 지정한 변수에 대입할 필요가 있다.
도트연산자는 컴파일 타임의 체크 기능을 높인다는 점에는 성공적인 기능일 것이다. 그러나 원래목적은 코드작성을 줄이고 편하게 하기 위한 목적으로 도입된 기능이지만만 빈번하게 형변환할 필요가 있으므로 결과적으로 늘어나버리고 있다.
실제로 프로퍼티를 사용해 프로그래밍을 하면 적절한 속성의 설정이 매우 중요하다는 것을 알아차릴것 이다.
속성의 문법
속성이라는 것은 프로퍼티를 수식하는 것이다. 프로퍼티를 설정한 코드를 컴파일하면 그것에 맞는 엑세스 함수를 만들어 주지만 함수의 종류나 동작에 대해 몇가지 설정할수 있는게 속성이다.
문법은 @property지시자의 다음에 괄호을 붙어셔 지정하는 것이 된다. 복수의 속성을 지정할때는 쉼포로 구분한다. 예를들면 다음의 코드에서는 assign과 readwrite라고하는 2개의 속성을 설정하고 있다.
@property (assign, readwrite) NSString* value;
어떤 속정이 정의되어있는지 상세하게 소개해 보겠다.
접근자의 이름과 종류
프로퍼티는 기본적으로는 도트연산자로 접근한다. 그러나 이런 접근은 결과적으로는 대응하는 엑세스함수를 호출하는것이 된다. 따라서 직접 함수를 호출하는 형태로 기술해도 결과는 같게된다.
그럼 이 엑세스함수는 컴파일러가 자동적으로 만들어 주는것인가. 그 이름은 어떻게 되는건가 이것이 키값 코딩의 규칙에 준거해 붙일수 있게된다. 키값코팅은 Mac OS X 10.3(Panther)의 시대에 Cocoa바인딩의 일부로해서 추가된 것이지만 지금은 Cocoa의 모든곳에서 사용되고있다.(키값코팅에 대해서는 『Tiger의Cocoa에보는MVC의완성』을 참고하길 바란다).
이렇게 자동적으로 붙여주는 이름 이외에 자기이름을 지정하는 것도 가능하다. 그 때문에 사용되는 것이 속성의 getter및 setter다. getter은 읽기 setter은 쓰기를 위한 엑세스의 이름을 지정한다. 다음과 같이 사용한다.
@property (getter=isEnabled, setter=makeEnable:) BOOL enabled;
getter함수는 인수는 없이 프로퍼티값을 돌려주는 것이고 setter함수는 프로퍼티 형의 인수를 1개받아서 리턴값은 void형이 되는 함수이다. 주의 해야할점은 setter은 인수를 받으므로 마지막의 콜론까지 해두지 않으면 안된다. Objective-C에서는 인수는 반드시 콜론 다음에 나온다. 콜론을 쓰는것을 잊어 버리면 컴파일이 되지않다.
프로퍼티는 디폴트로 읽기쓰기가 가능하다. 읽기전용을 하고 싶은 경우도 있을것이다. 그럴때에는 readonly속성을 사용ㅎ나다. 이것을 지정한 프로퍼티는 읽기정용으로 되고 도트연산자로 쓰기를 시도하면 컴파일 에러가된다. 이걸 함수로해서 쓰려고해도 대응하는 함수가 만들어져 있지 않기 때문에 실행중 에러가 된다.
프로퍼티가 읽기쓰기 가능인 것을 명시적으로 해두고 싶은 경우는 readwrite속성을 사용하는 것이 된다.
@property (readwrite) NSString* fullName;
@property (readonly) NSString* firstName;
설정된 오브젝트의 보관
프로퍼티의 형이 오브젝트이고 읽기쓰기가 가능한 경우 그것을 어떻게 보관할까가 문제가 된다. 뭐가 문제가 되는가는 가비지콜렉션을 유효로 하고있는가 아닌가에 따라 다르다.
우선 가비지콜렉션을 무효로하고잇는 경우부터 설명하겠다. 이 경우는 오브젝트의 소유권을 어떻게 해야만 하는가를 신경쓰지않으면 안된다. 즉 retain을 홀출해 그 오브젝트를 보관할것인가 혹은 단순히 참조만 하것인가를 결정해야한다.
이 동작을 프로퍼티의 속성으로 지정하는 것이 가능하다. retain을 호출할때는 retain속성 단순히 참조를 할때는 assign속성을 지정한다.
@property (retain) NSString* title;
@property (assign) id delegate;
결국 위에 표시한 예는 다음의 소스코드와 동등하게 된다.
// retain속성을 지정한 경우
- (void)setTitle:(NSString*)title
{
if (_title != title) {
[_title release];
_title = [title retain];
}
}
// assign속성을 지정한 경우
- (void)setDelegate:(id)delegate
{
_delegate = delegate;
}
가비지 콜렉션을 유효로 하는 경우는 앞과 같은걸 신경쓸 필요가 없다. retain 함수는 비활성화 되고 참조 하고있는가 어떤가에 그 오브젝트가 메모리 해제 될건지 어떤지가 결정되기 때문이다.
@property (copy) NSString* value;
대응하는 코드는 다음과 같이 될것이다.
- (void)setValue:(NSString*)value
{
[_value release];
_value = [value copy];
}
이 retain、assign、copy의 사용방법이지만 조금 까다롭다. 우선 가비지 콜렉션이 무효인 상태에서는 반드시 어떤거든 1개는 지정하자. 지정 하지 않으면 컴파일 타임에 경고가 발생하고, assign이 적용 되어진다.
그에 비해 가비지 콜레션이 유효인 경우는 꼭 지정하지는 않아도 괜찮다. 그경우 assign이 된다. 대게 이게 싫은거지만 속성을 설정하지 않는 경우에 그 프로퍼티의 형이 NSCopying프로토콜에 준거 하고 있으면「속성이 지정되있지 않기 때문에 assign을 지정하지만 이 오브젝트는 복사가 가능 하기때문에 복사하지 않아도 괜찮은 것인가?」라는 의미의 경고가 발생한다.
결국 가비지 콜랙션이 유효인 경우도 assign인가 copy인가를 명시적으로 지정해 두는편이 편한것이다. 가비지 콜렉션을 무효로 해서 retain이랑 copy을 지정한 경우는 dealloc함수로 이것들을 release하는것을 잊지않도록 하자