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이되어야 한다고 컴파일러에게 알려주는 것이다. 이 옵션이 없으면 기본적으로 인스턴스 변수의 이름은 프로퍼티 이름과 동일하게 설정된다. 변수 이름앞에 언더스코어를 붙이는 이 패턴은 이미 Objective-C의 기본 패턴이 되었으며, 현재의 LLVM 컴파일러들은 아예 @synthesize 선언이 없어도 자동으로 프로퍼티 접근자를 생성한다.

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

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

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

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

느긋한(lazy) 초기화

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

LLVM 컴파일러는 모든 객체 타입 프로퍼티는 nil로, 그외 C 원시 타입은 0으로 자동으로 초기화한다. 따라서 init 메소드 등에서 프로퍼티를 초기화할 필요를 가능한 줄인다. 실제로 문서상에서 명시적으로 언급하지는 않지만, 언어와 컴파일러의 모든 디자인은 init 내에서 프로퍼티를 초기화하는 것을 그리 장려하지는 않는다. 만약 초기값이 nil이 아닌 다른 객체여야 한다면? 그런 경우에는 init 메소드 내에서 초기화해야 할 것이다. 하지만 이보다 조금 더 권장되는 방법이 있으니, 바로 느긋한 초기화이다.

느긋한 초기화는 객체 스스로가 의존하지 않는 프로퍼티에 대해서 객체의 자신의 초기화와 상관없이, 프로퍼티의 첫 액세스 시점에 해당 프로퍼티를 초기화하는 것을 말한다. 전형적으로 다음과 같은 방식으로 getter 메소드를 작성한다.

- (MyDataProvider*) dataProvider {
  if (!_dataProvider) { // 스토리지변수가 nil이면 초기화한다.
    _dataProvider = [[MyDataProvider alloc] init];
   }
  return _dataProvider;
}

만약 특정 프로퍼티의 객체가 생성비용이 매우 비싸다면 (대표적으로 NSDataFormatter 가 생성 비용이 큰 클래스로 꼽힌다) 해당 프로퍼티를 사용하지 않는 경우에 낭비되는 시간과 리소스를 절약할 수 있다. 참고로 iTunes에서 볼 수 있는 스탠포드 대학의 iOS 강좌(Objective-C로 진행된 이전 강좌)에서는 거의 모든 객체 프로퍼티를 이런식으로 느긋하게 초기화하였다.

점(dot) 문법

개인적으로 점 문법은 일종의 필요악이라 생각한다. 사실 Objective-C는 C이고, 객체 인스턴스는 구조체의 포인터이다. (구조체가 아니다.) 따라서 self.someProp과 같은 표현은 C 문법상 잘못된 것이다. 하지만 컴파일러는 객체에게 dot 문법이 쓰이는 경우에 그것을 접근자 메소드로 치환해서 컴파일하는 기능을 지원한다. 예를 들자면 다음의 예에서 연속한 두 라인은 모두 같은 의미이다.

NSLog(@"Tom's address: %@", tom.address);
NSLog(@"Tom's address: %@", [tom address]);

tom.address = @"...";
[tom setAddress:@"...."];

tom이 객체인 경우에 .을 사용한 문법이 예외적으로 프로퍼티를 액세스하는 것처럼 사용될 수 있다. 이것은 문법적 허용이 아니라, 컴파일러의 전처리 과정에서 각각의 예시의 앞 라인을 뒤 라인으로 변형하여 컴파일한다. 개인적으로는 “실제로 무슨일이 일어나는지 알지 못한다면” 점 문법을 쓰지 말 것을 권한다. 이런 문법을 편의상 허용하는 것까지는 이해를 하겠는데, 메소드 호출까지 이런식으로 쓰려는 실수를 하는 초보자들도 많기 때문에, 가능하면 점 문법은 쓰지 말자.

정리

선언 프로퍼티는 클래스를 정의할 때 부수적으로 발생할 수 있는 노가다를 줄이기 위해서 인스턴스 변수와 접근자 메소드들의 이름을 특정한 규칙에 의거해서 관계를 짓게 하고, 이를 별도의 컴파일러 지시어를 통해서 선언만 해 두면 자동으로 필요한 코드를 컴파일러가 생성하도록 해주는 것이다. 또 여기서 사용되는 이름규칙은 키밸류 코딩이라는 Objective-C의 강력한 기능의 근간이 되고 다시 키밸류코딩은 키밸류옵저빙, 코코아 바인딩 나아가서 코어데이터까지 연결되는 중요한 컨셉이다. 프로퍼티 개념에 대해서 정확한 이해를 갖는데 이 글이 도움이 되었길 바란다.