iOS앱 만들기, Objective-C, 스터디

ObjC 프로퍼티 기초

이해하기 쉬운 Objective-C 프로퍼티

Objective-C 클래스에서 가장 기본이 되는 프로퍼티(Declaired Property)에 대해 몇 가지 글을 쓴 적이 있는데, 오늘은 이 내용을 좀 더 쉽고 구체적으로 풀어보고자 한다.

프로퍼티를 잊자

일단 프로퍼티 따위 생각하지 말고, Person 이라는 클래스를 만들어보자. 이 클래스는 이름, 성, 나이, 이메일주소를 저장하는 클래스이고, NSObject로부터 상속받는 것들을 제외하고는 아무런 메소드를 일단은 주지 않기로 한다.

일단 Objective-C의 클래스는 일종의 구조체로 보면 무방하기 때문에 다음과 같이 선언할 수 있다. 1

@interface Person: NSOject 
{
    int age;
    NSString * firstName;
    NSString * lastName;
    NSString * email;
}
@end

@implementation Person
@end

이 클래스를 생성하는 것은 가능하다. 왜냐면 alloc, init 같은 메소드는 NSObject로부터 상속 받았기 때문이다.

Person *person = [[Person alloc] init];

하지만 애석하게도 이 person의 인스턴스 변수인 age라든지 firstName등에는 전혀 액세스할 수가 없다. 왜냐면 인스턴스 변수 자체는 private하기 때문에 객체 외부에서는 액세스할 방법이 없기 때문이다.

NSLog(@"%d", person.age); ==> ERROR!

접근자 메소드를 만들자

만약 age라는 인스턴스 변수에 대해서 객체 외부에서 접근하고 싶으면 객체가 제공하는 인터페이스를 통해야 한다. 두 개의 메소드를 추가해보자. 각각의 이름은 getAge, setAge이며 이름이 의미하듯, 하나는 age를 꺼내오는 용도, 다른 하나는 age에 값을 넣는 용도로 쓸 것이다.

객체 외부로 알려질 메소드는 인터페이스 선언부에서 프로토타입을 선언해준다.

@interface Person: NSOject 
{
    int age;
    NSString * firstName;
    NSString * lastName;
    NSString * email;
}

- (int)getAge;
- (void)setAge:(int)newAge;
@end

그리고 각 메소드의 구현은 다음과 같다.

@implementation Person
- (int)getAge {
    return age;
}

- (void)setAge:(int)newAge {
    age = newAge;
}
@end

이제 다음과 같은 접근이 가능해질 것이다.

Person *person = [[Person alloc] init];
[person setAge:20];
NSLog(@"%d", [person getAge]);

자 이제 age 라는 int 타입의 멤버를 클래스에 추가하고, 이를 외부에서 액세스하게 하려면 다음과 같은 코딩이 필요하다.

  1. 인터페이스 선언부에서 해당 멤버 변수를 하나 선언한다.
  2. 인터페이스 선언부에서 해당 멤버 변수에 접근할 수 있는 getAge, setAge: 메소드를 선언한다.
  3. 크래스 구현부에서 두 메소드에 대한 정의를 작성한다.

그러니까 최소한 멤버 하나당 다섯줄의 코딩이 필요한 셈이다. 만약 멤버가 다른 객체 인스턴스라면 좀 더 머리아파지는 상황이 온다. 예를 들어 firstName을 생각해보자. 인터페이스 선언을 getFirstName, setFirstName:이라고 했다고 가정한다.

- (NSString *)getFirstName {
    return firstName;
}

- (void) setFirstName:(NSString *)newFirstName {
    // 기존에 가지고 있던 것 릴리즈 
    [firstName release];
    firstName = [newFirstName copy];
}

이렇게 썼다고 하자. 음 그런데 이 코드는 문제가 있는 코드다. 왜냐면 [person setFirstName:[person getFirstName]] 이라고 실행하면, 복사하기 전에 릴리즈해버리기 때문에 제대로 동작하지 않는다. 따라서 다음과 같이 수정해야 한다.

- (void) setFirstName:(NSString *)newFirstName {
    [newFirstName retain];
    // 기존에 가지고 있던 것 릴리즈 
    [firstName release];
    firstName = [newFirstName copy];
    [newFirstName release];
}

인자로 받은 객체를 날려버리지 않도록 세심한 주의가 필요하다. 그런데 보통 클래스의 멤버들은 C 원시타입보다는 객체 인스턴스가 많다. 그럼, 만약 20개의 멤버를 가지고 있는 클래스를 만든다고 하면 백라인이 넘는 코드가 단지 이러한 멤버를 get하고 set하는데만 필요하다는 이야기이다.

프로퍼티 선언

그런데 이런 getter, setter 프로퍼티들은 몇 가지 공통점이 있다.

  1. 만약 당신이 창의력을 주체할 길이 없는 씽크빅 대장이 아닌 이상, 멤버의 이름이 age라면 보통 접근자 메소드의 이름은 getAge, setAge로 지을 것이다.
  2. 멤버의 타입에 따라서 get하거나 set 할 때 동작해야 하는 코드는 거의 정형화되어 있다.

그래서 Objective-C는 일종의 편의 기능으로 프로퍼티 선언이라는 것을 제공한다. 룰은 다음과 같다.

  1. 인터페이스 선언부에서 @property 지시어를 사용해서 멤버의 이름, 타입, 액세스 방식등을 선언한다.
  2. 클래스 구현부에서 @synthesize 지시어를 사용해서 선언된 프로퍼티에 대해 접근자 메소드를 “합성하도록 한다”

그리고 컴파일러는 다음과 같은 일을 해준다.

  1. @property 지시어를 만나면 해당 지시어가 가리키는 타입, 이름의 변수2를 하나 선언하고, 그에 대한 접근자 메소드를 자동으로 선언한다. (이 과정은 사실 거의 단순한 자동 문자열 변환이나 다름없다.)
  2. @synthesize를 만나면 미리 정해진 규칙을 가진 접근자 메소드가 별도로 정의되어 있지 않다면 자동으로 코드를 생성해준다.

즉 프로퍼티 선언은 멤버변수 선언부터 시작해서 접근자 메소드를 컴파일러가 대신 작성해주도록 할 수 있는 것이다. 만약 특별한 액세스 전/후 처리가 필요하다면 개발자가 직접 코드를 작성해주면 컴파일러는 이를 존중하여 자동으로 메소드를 만들지 않는다.

그럼 클래스 정의를 다시 아래와 같이 바꿔보자.

@interface Person: NSOject 
@property (assign, nonatomic) int age;
@property (copy, nonatomic) NSString *firstName;
@property (copy, nonatomic) NSString *lastName;
@property (copy, nonatomic) NSString *email;
@end

@implementation Person
@synthesize age, firstName, lastName, email;
@end

자 이렇게하면 다음과 같은 것들이 만들어진다.

  1. 각 프로퍼티 이름과 동일한 인스턴스 변수가 선언된다. (보통 이름앞에 언더스코어가 붙는다)
  2. 각 프로퍼티 이름과 동일한 이름의 getter 메소드가 선언된다. 인스턴스 변수 _age에 대한 getter 메소드는 -(int)age이다.
  3. 각 프로퍼티의 첫글자를 대문자로 바꾸고 그 앞에 set을 붙인 이름의 setter 메소드가 만들어진다. age의 경우 -(void)setAge:(int)age가 될 것이다.

그리고 그 구현은 각각

- (int)age {
    return _age;
}

- (void)setAge:(int)age{
    _age = age;
}

이런식이 될 것이다.

결론

선언 프로퍼티는 클래스내의 멤버3에 대한 접근자를 자동으로 만들어주는 기능이다. 하지만 이보다 중요한 것은 언어 자체가 지원하는 확장 문법인데,

  1. person.age와 같은 식으로 사용이 가능해진다. 컴파일러는 이 문법을 [person age]로 바꿔서 인식한다. 또한 프로퍼티로 선언하면 person.age = 30;과 같이 setter도 구두점 문법으로 쓸 수 있다.
  2. 구두점 문법이 가능한 이유는 getter, setter 메소드의 모양이 프로퍼티 이름을 기준으로 정해진 규칙을 따르기 때문이다. 만약 프로퍼티를 쓰지 않고 수동으로 접근자 이름을 이에 맞게 만들었다면 역시 구두점 문법으로 액세스가 가능해진다.
  3. 프로퍼티의 이름과 인스턴스 변수 및 접근자 메소드의 이름을 정하는 규칙을 따르는 것을 “키-밸류 코딩 규약을 따른다”라고 표현한다. 각 이름들이 연결되는 규칙을 따르면 Foundation 프레임워크가 지원하는 강력한 선물인 키-밸류 코딩이나 키-밸류 옵저빙을 지원하는 클래스를 공짜로 만들게 된다.

더 나아가서

@synthesize age=__age 이런식으로 프로퍼티가 저장될 인스턴스 변수의 이름을 지정하는 것도 가능하다. 또 @property 뒤에 괄호에 들어가는 키워드들은 프로퍼티의 실제 접근자를 생성할 때 동작방식을 설정하는 부분이다. 이는 다른 글을 참조하도록 한다.


  1. 단 구조체와는 달리 멤버에 대한 직접 접근은 불가능하다. 
  2. 컴파일러 마다 다를 수 있다. Xcode가 쓰는 gcc나 clang의 경우에는 인스턴스 변수까지 자동으로 선언해준다. 하지만 GNUStep을 쓰고 gcc를 쓰는 경우에는 인스턴스 변수까지는 수동으로 설정해주어야 하더라. 
  3. Objective-C는 C와 사실 같은 언어이고, C에서 ‘멤버’는 구조체 내부에 들어있는 변수로, 외부로 공개되어 있다. 따라서 클래스에서는 ‘멤버’라는 표현을 쓰지 않고 구분하여 ‘인스턴스 변수’라고 한다. 흔히 ‘ivar’로 표현되니 참고할 것.