Objective-C의 선언 프로퍼티 (Declared Property)에 대해

Objective-C의 객체 인스턴스에 어떠한 변수 값을 포함하고자 한다면 클래스 내에 인스턴스 변수를 선언하고, 여기에 값을 저장할 수 있다. (흔히 애플 문서등에서는 이런 인스턴스 변수를 ivar라 한다.)

기본적으로 객체의 내부에서 선언되는 인스턴스 변수는 private하며 객체의 외부에서는 내부의 인스턴스 변수값에 액세스하는 것이 차단된다. 따라서 객체의 외부에서 인스턴스 변수의 값을 읽거나 쓰기 위해서는 클래스가 해당 인스턴스를 읽게하거나, 쓰게 해주는 API를 제공해야 한다. 이렇게 객체가 자신의 내부 속성값에 대해 읽거나 쓰게 하기 위해 제공하는 메소드를 접근자(accessor) 메소드라고 한다.

객체 내부의 속성값에 액세스할 수 있게 하는 방법, 프로퍼티

접근자 메소드와 인스턴스 변수의 조합으로는 다음의 몇 가지 조합이 가능할 수 있다.

  1. 내부에 인스턴스 변수를 선언하고, 이 값을 읽을 수 있는 메소드와 이 값을 쓸 수 있는 메소드를 각각 제공한다. (read and write)
  2. 내부에 인스턴스 변수를 선언해두고, 이 값을 읽을 수 있는 메소드만 제공하며, 이 값을 쓸 수 있는 메소드는 제공하지 않는다. ( read only)
  3. 내부에 인스턴스 변수의 값을 읽는 것이 아닌, 다른 속성들로부터 계산된 값을 리턴하는 메소드를 제공한다. (computed)
  4. 내부에 인스턴스 변수를 선언하고, 외부에서는 쓰기 메소드만 제공한다. (당신은 변태입니다.)

어떤 Objective-C의 객체가 있을 때, 우리가 할 수 있는 일은 그 객체에 대해서 어떤 메시지를 보내는 것 밖에 없다. 사실 “메시지를 보낸다”는 말과 “메소드를 호출한다”는 말이 거의 동치이기 때문에 되려 Objective-C 의 개념을 이해하는데 어려움을 겪는 경우가 있는데, 프로퍼티가 바로 그런 것 중의 하나이다. 어떤 객체의 속성값을 읽기 위해서는 그 속성값을 내놓으라는 메시지를 보내야 하고, 속성값을 변경하기 위해서는 그 속성값을 변경해달라는 메시지를 보내야 한다.

프로퍼티는 속성값 자체가 아닌 속성값에 접근하는 방법을 말한다

결국 Objective-C의 객체 외부에서 해당 객체와 상호작용할 수 있는 방법은 메시지 전달(메소드 호출) 밖에 없다. 따라서 프로퍼티란, 특정한 값을  읽거나 쓰기 위해 존재하는 메소드, 혹은 메소드의 쌍이며 필요에 따라서 내부 구현은 위에서 언급한 케이스들로 나뉠 수 있다.

대신에 사용상의 편의를 위해서 이러한 접근자 메소드들의 이름은 어떤 관계적인 규칙을 따라서 명명되고, 이러한 규칙에 따라서 명명된 접근자 메소드(들)을 프로퍼티라고 한다.

기본적인 프로퍼티 구성

예를 들어 Person이라는 클래스에 이름과 나이를 나타내는 두 프로퍼티를 정의한다고 가정해보자, 각각의 이름은 name, age 로 정할 수 있다.

먼저 인스턴스 변수를 정의한다. 이 때 인스턴스 변수의 이름은 사실 어떤 것이어도 상관없다. 대신 관례상 프로퍼티 이름과 동일하거나 혹은 언더 스코어를 붙인 _name이라는 식으로 정의한다.

그리고 이를 액세스하기 위한 두 개의 접근자 메소드가 필요하다. 이 때 규칙은 다음과 같다.

  1. 읽기를 위해 사용하는 getter 접근자는 프로퍼티 이름과 동일한 이름을 쓴다.
  2. 쓰기를 위해 사용하는 setter 접근자는 set프로퍼티이름: 의 형태로 작성한다. 이 때 set 다음에 오는 프로퍼티 이름은 대문자화하여 적용한다.

따라서 아래와 같이 name 이라는 프로퍼티를 헤더에 선언할 수 있다.

@interface Person : NSObject
{
    NSString *_name;
    int _age;
}
-(NSString *)name;
-(void)setName:(NSString *)name;
@end 

이제 해당 프로퍼티를 위한 두 개의 접근자 메소드를 작성해보자.

@implementation Person
- (NSString *)name {
    return _name;
}

- (void)setName: (NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name copy];
    }
}

여기까지 보면 특별할 것이 별로 없다. 내부 속성을 획득해서 그대로 리턴해주는 메소드 하나와 새로운 값으로 내부 인스턴스 변수의 값을 교체해주는 메소드하나. 그런데 이것은 그냥 “접근자”라고만 해도 될 것을 왜 프로퍼티란 용어를 사용할까?

선언 프로퍼티

프로퍼티의 진정한 의의는 이렇다. 클래스를 하나 디자인하다보면 내부 속성값이 한 두 개가 아닌 경우가 많다. 이런 모든 경우에 대해서 접근자 메소드를 (특별한 경우가 아닌 이상) 두 개씩 추가적으로 정의하고 또 구현해야 한다. 심지어 이 코드들은 이름만 다를 뿐이지 거의 같은 내용으로 구성될 것이다.

이런 노가다를 해소하기 위해 도입된 것이 선언 프로퍼티이다. 선언 프로퍼티는 특별한 지시어를 사용해서, 인스턴스 변수의 정의부터 접근자 메소드의 정의와 구현을 컴파일러가 자동으로 코드를 생성하도록 해주는 것이다.

다음 예제는 앞서 작성했던 코드를 선언 프로퍼티를 통해서 재작성한 것이다.

// Person.h
@interface Person : NSObject
@property (copy, nonatomic) NSString *name;
@end

// Person.m
@implementation Person
@synthesize name=_name;
@end

@synthesize에서 name=_name 이라고 한 것은 이 프로퍼티를 위한 인스턴스 변수의 이름이 _name이되어야 한다고 컴파일러에게 알려주는 것이다. 이 옵션이 없으면 기본적으로 인스턴스 변수의 이름은 프로퍼티 이름과 동일하게 설정된다.

즉 프로퍼티는 내부 인스턴스 변수와 두 개 혹은 한 개의 접근자 메소드가 특정한 이름을 기준으로 정해진 패턴의 형태를 갖는다는 제약을 암묵적으로 정한 후, 프로퍼티 선언과 합성 (synthesize) 지시어를 만나면 이에 대응하는 해당 코드를 직접 만들어주는 것이다. (그래서 “합성해낸다”는 뜻의 @synthesize를 쓴다.)

프로퍼티 선언시 기재하는 속성들

프로퍼티의 속성은 setter와 getter를 자동으로 생성할 때 필요한 정보를 컴파일러에게 알려주는 힌트이며 @property 뒤에 괄호와 함께 온다. 이중 몇 가지를 살펴 보도록 하겠다.

  • atomic / nonatomic : 해당 속성을 atomic 하게 접근하게 할 것인지를 결정한다. atomic 한 접근은 해당 값에 쓰기를 할 때 락을 걸어서 여러 스레드에서 동시에 액세스할 때 예기치 않게 데이터가 파괴되는 상황을 방지할 수 있다. 하지만 그만큼 로드가 많이 걸리게 되므로 특별한 경우를 제외하고는 잘 쓰이지 않는다.
  • copy : setter 에서 주어진 값을 복사한 새로운 사본 객체를 저장하도록 한다.
  • strong / weak : strong은 예전에는 retain 으로도 썼다. setter에 주어진 객체에 대해서 강한 참조를 만들어서 객체 외부에서 해당 값이 해제되는 것을 방지한다. weak를 쓰면 약한 참조를 가리키게 되며, 객체 외부에서 해당 값이 해제되는 경우가 발생할 수 있다. 이 경우 해당 값은 자동으로 nil이 된다. weak는 두 객체가 서로를 참조할 때 순환참조에 의한 메모리 누수를 막기위해 사용한다.
  • readwrite / reaonly : setter 메소드를 자동 생성할 것인지를 결정한다. 생략하는 경우 디폴트는 readwrite가 된다.
  • setter= / getter= : 기존의 이름 컨벤션을 따르지 않는 별도의 접근자를 생성하도록 한다. 특히 ARC가 도입되면서 프로퍼티 이름이 new-로 시작하는 경우에 Xcode가 에러를 내게 되었다. (경고가 아니다) 이 경우에는 접근자 이름을 다른 것으로 설정하여 해당 에러를 피할 수 있다.

오버라이드

프로퍼티를 선언하고 구현부 파일에서 @synthesize를 썼다 하더라도, 명시적으로 접근자 메소드를 작성하였다면 컴파일러는 프로퍼티 합성 지시어를 무시하고 프로그래머가 작성한 접근자를 우선으로 취급한다. 따라서 인스턴스 변수를 따로 선언하기 보다는 항상 프로퍼티로 내부 속성을 정의하는 습관을 들이도록 한다.

특히 setter는 컴파일러에게 위임하는 대신에 getter만 직접 작성하는 것도 가능하다. 특히 프로퍼티의 초기값을 init 메소드에서 지정하는 것이 아니라, 해당 프로퍼티의 getter가 최초로 액세스되는 시점에 초기화하는 테크닉을 사용하면 객체의 초기화 시간을 단축할 수 있고, 경우에 따라서는 불필요한 메모리 점유도 막을 수 있다.

특히 Objective-C 2.0 부터는 컴파일러가 원시타입 값은 0으로 객체 타입 값은 모두 nil 로 인스턴스 변수를 자동으로 초기화한다.

예를 들어 다음과 같은 식으로 getter만 작성하는 것이 lazy 프로퍼티의 전형적인 예이다.

- (MyDataProvider*) dataProvider {
  if (_dataProvider == nil) {
    _dataProvider = [[MyDataProvider alloc] init];
   }
  return _dataProvider;
}

init 내에서 _dataProvider를 초기화하는 과정을 생략하면 그만큼 초기화 시간을 절약할 수 있고, 만약 이 객체가 생성비용이 매우 비싸다면 (대표적으로 NSDataFormatter 가 생성 비용이 큰 클래스로 꼽힌다) 해당 프로퍼티를 사용하지 않는 경우에 낭비되는 시간과 리소스를 절약할 수 있다.

참고로 iTunes에서 볼 수 있는 스탠포드 대학의 iOS 강좌(Objective-C로 진행된 이전 강좌)에서는 거의 모든 객체 프로퍼티를 이런식으로 느긋하게 초기화하였다.

점(dot) 문법

혼란을 피하기 위해서 객체 인스턴스의 프로퍼티에 접근하는 것은 늘 명시적으로 접근자 메소드의 형태를 쓰는 것을 권장하지만, 이는 마치 .을 통해서 프로퍼티에 액세스하는 것이 C 구조체의 멤버에 접근하는 것처럼 보이기 때문이다. 하지만 Objective-C의 인스턴스 변수는 외부로부터 완전하게 은닉되며, 오직 접근자 메소드를 통해서만 액세스할 수 있다.

실제로는 점문법을 통한 액세스가 허용되며, 이 경우는 인스턴스 변수를 직접 액세스하는 것이 아니고 접근자 메소드 호출로 동작하게 된다.

정리

선언 프로퍼티는 클래스를 정의할 때 부수적으로 발생할 수 있는 노가다를 줄이기 위해서 인스턴스 변수와 접근자 메소드들의 이름을 특정한 규칙에 의거해서 관계를 짓게 하고, 이를 별도의 컴파일러 지시어를 통해서 선언만 해 두면 자동으로 필요한 코드를 컴파일러가 생성하도록 해주는 것이다.

또한 이러한 이름 규칙은 Objective-C에서 매우 중요한 기능인 키-밸류 코딩과 키-밸류 옵저빙을 위한 기본적인 조건이 된다. 즉 외부에서 액세스 가능한 모든 속성은 프로퍼티로 선언하는 것이 타이핑 양도 줄이고, KVC, KVO에 부합하는 클래스로 자동으로 만들어지게 된다는 것이다.