프로퍼티 상속 파고들기 – Swift

Swift의 클래스 상속과 관련하여 이니셜라이저의 동작 규칙은 안정성이라는 측면을 아주 중요하게 여기고 있어서 몇 가지 규칙이 강제되고 있다. 이 규칙은 컴파일러에 의해서 강제되고 있기 때문에 이니셜라이저를 작성할 때 주의를 기울여야 하고, 그렇지 않으면 언뜻 보기에 아무런 문제가 없는 것 같은 코드가 컴파일이 되지 않는 경우가 많다.

이니셜라이저와 더불어서 프로퍼티 역시 작성할 때 주의해야 할 부분이 몇 가지 있다. 문제는 이니셜라이저와 마찬가지로 클래스를 상속하는 상황에서 프로퍼티 상속과 관련되는 케이스가 많아지고 헷갈리는 상황을 제법 접할 수 있게 된다는 것이다. 이번 글에서는 프로퍼티의 상속에 관한 여러 사례들을 검토해보도록 하자.

프로퍼티 상속 파고들기 – Swift 더보기

(파이썬) 약한 참조 사용하기

흔히 파이썬과 C와의 차이점을 묻는 질문이 많은데, 이런 저런 답 중에서 “파이썬은 메모리 관리가 필요없다”는 것도 있다. 파이썬에서 실행시간에 생성된 객체는 자동으로 관리되어 “더 이상 쓸모가 없어지면” 자동으로 파괴된다. 따라서 파이썬에서는 명시적으로 객체를 파괴하는 코드를 작성하지 않는 것이 보통이다.

파이썬의 이러한 특성 때문에 파이썬의 메모리 관리는 가비지 콜렉터에 의해서 관리된다는 믿음이 있는데, 이것도 (어느 정도 예전에는 사실이었으나) 진짜는 아니다. 파이썬은 Objective-C와 비슷하게 참조수(Reference Count)기반의 자동 메모리 관리 모델을 따르고 있다. 파이썬의 모든 변수는 값을 담는 영역이 아니라 객체에 바인딩 되는 이름이다. 객체와 이름이 바인딩되면, 해당 객체는 그 이름에 의해 참조되는 것이고 이는 그 참조수를 1만큼 증가시키는 작용을 한다.

그 이름이 더 이상 해당 객체를 가리키지 않게 되는 경우, (변수 스코프를 벗어나거나 다른 객체에 바인딩 되는 경우) 참조수는 1이 감소하고, 그 결과로 참조수가 0이되면 해당 객체는 가비지 콜렉터의 도움없이 그 자리에서 즉시 파괴된다.  이 글에서는 파이썬의 이러한 메모리 관리방식으로부터 발생할 수 있는 함정, 메모리 누수가 생길 수 있는 케이스와 어떻게 이를 회피할 수 있는지를 알아보도록 하자.

(파이썬) 약한 참조 사용하기 더보기

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

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

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

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

atomic 프로퍼티의 접근자 메서드

많은 iOS책들에서 프로퍼티는 대부분 nonatomic 속성으로 지정한다. 이 때 nonatomic은 항상 명시하는데, 이는 Objective-C에서 프로퍼티의 디폴트는 atomic이기 때문이다. 그런데 이게 정말, nonatomic이어야 하기 때문에 이렇게 하는 것인지, 맞는 것인지는 좀 아리송한데…

atomic은 예전에도 한 번 설명한 적이 있는데, 접근자를 호출한 스레드에서 그 프로퍼티를 읽거나 쓰는 중간에는 다른 스레드나 큐에서 액세스 하지 못하도록 한다는 의미이다. 따라서 멀티 스레드나 백그라운드 작업을 염두에 둔다면 atomic 속성에 대해서도 고려를 해야 한다는 의미이다.

어떤 스레드에서 객체의 프로퍼티에 접근할 때 다른 스레드에서의 접근을 막는 것을 공교롭게도 “동기화”라고 표현하더라. 정말 와닿지 않는 표현이다. 서로 다른 스레드에서 접근할 때 같은 값을 돌려주는 것을 보장하는 것도 아닌데 말이다. 어쨌든, atomic으로 설정한 프로퍼티의 setter나 getter는 다음과 같은 모양이 될 것으로 추측된다.

-(NSNumber*)myIntegerValue
{
    @synchronized(self) {
        return _myIntegerValue;
    }
}
-(void)setMyIntegerValue:(NSInteger*)newIntegerValue;
{
    @synchronized(self) {
        _myIntegerValue = newIntegerValue;
    }
}

저렇게 synchronized(self) 라고 하면 저 블럭 사이의 코드가 실행될 때는 self 전체가 이 접근자를 호출한 스레드에 대해 atomic해진다. 다른 스레드에서 접근할 때에, 이 스레드는 요 블럭 사이의 코드가 모두 실행을 완료할 때 까지 기다리게 된다.

일반적으로 atomic하게 선언된 프로퍼티에 대해 접근자 메소드를 커스터마이징하려고 하면 오류가 난다. 그래서 왠만하면 nonatomic으로 사용하는게 역시나 정신 건강에 좋을 것 같다. 물론 atomic한 접근이 필요한 케이스도 분명 있지만, 내가 저런 경우를 만날만한 앱을 만들일은 없을 것 같다.

키밸류 코딩이란

NSObject는 Objective-C의 표준 라이브러리라 할 수 있는 Foundation에서 가장 기본이 되는 최상위 클래스에 해당한다. 커스텀 클래스를 만들 때 아무 생각없이 상속받는 이 클래스는 Objective-C에서 클래스라는 것이 마땅히 갖추어야 하는 여러 가지 기능들을 미리 구현해둔 것이 아주 많이 있다. 그 중에서도 키밸류 코딩이라는 기술을 위한 기본적인 기능이 NSKeyValueCoding이라는 비정규 프로토콜에 정의되어 있고, NSObjects는 이를 따르고 있다. 따라서 몇가지 간단한 규칙을 지키면서 프로퍼티를 정의하기만 하면, 우리가 작성하는 모든 클래스의 프로퍼티들이 키밸류 코딩 호환이 될 수 있다. 그렇다면 키밸류 코딩은 무엇이고, 또 어떻게 활용되는 것인지에 대해서 살펴보자.

프로퍼티

키밸류 코딩은 어떠한 객체의 프로퍼티 값에 대해서 미리 정해진 접근자가 아닌 해당 프로퍼티의 이름 키를 사용해서 특정한 객체의 프로퍼티를 액세스하는 것을 말한다. 예를 들어서 어떤 클래스 Foo 에서 bar 라는 프로퍼티를 가지고 있다고 가정하고, 클래스 Foo를 작성하는 과정을 살펴보자. 먼저 Objective-C에서 어떤 클래스가 임의의 값을 저장하고 있으려면 그 값을 저장할 스토리지 변수가 필요하다. Objective-C의 클래스는 본질적으로 그 내부를 알 수 없는 불투명 구조체의 포인터이며, 구조체 내부의 멤버 변수는 인터페이스 선언부 최상단에 블럭을 사용해서 선언한다. 그리고 이렇게 선언된 멤버 변수는 외부와 완전히 격리되면서 외부에서는 액세스할 수 없고, 어떤 멤버 변수를 가지고 있는지 조차 알 수 없다. (이렇게 선언된 멤버 변수는 인스턴스 변수라 하고 흔히 ivar 라 지칭한다.) 따라서 이 변수에 값을 세팅하거나, 변수 값을 알아낼 수 있는 두 개의 메소드가 필요하다.

@interface Foo: NSObject
{
  NSString* _bar;
}
- (NSString*)bar;
- (void)setBar:(NSString*)newValue;
@end

Foo의 외부에서 해당 프로퍼티를 bar라는 이름으로 액세스하고, bar를 세팅하는 메소드를 -setBar:라고 이름붙였다. 멤버변수의 이름을 사실상 무엇이 되더라도 무관한데, 관습적으로는 getter의 이름과 똑같이 하거나 그 앞에 언더스코어를 붙인다. (언더스코어를 붙이는 이름이 멤소드 이름과 혼동을 줄이기 때문에 조금 더 권장된다.)

만약 이 bar라는 프로퍼티가 copy 시멘틱을 따른다고 하면, 두 메소드의 구현은 다음과 같이 작성될 것이다.

@implementation Foo
/// 초기화 시에 ivar를 초기화한다.
- (instancetype)init {
  self = [super init];
  _bar = nil;
}

- (NSString*)bar { return _bar; }
- (void)setBar:(NSString*)newValue]
{
  NSString* newBar = [[newValue copy] retain];
  [_bar release];
  _bar = newBar;
}
...
@end

즉 어떤 오브젝트가 그 내부에 어떤 값을 저장할 수 있고, 객체 외부에서 그 값을 액세스하려고 한다면 이 클래스는 다음의 세 가지 조건을 갖추어야 한다.

  1. 값을 저장할 수 있는 스토리지 변수
  2. 스토리지 변수를 액세스할 수 있는 getter 접근자
  3. 스토리지 변수를 업데이트할 수 있는 setter 접근자

만약 getter/setter 접근자가 모두 없는 경우라면, 해당 ivar는 클래스 내부에서만 참조할 수 있고, 외부에서는 액세스할 수 없는 값이 된다. 또 getter 메소드만 제공되는 경우라면, 객체 외부에서는 그 값을 getter 메소드를 통해서 읽을 수는 있지만 업데이트를 할 수 없는 읽기 전용의 값이 될 것이다. 이것이 Objective-C의 선언 프로퍼티의 핵심 내용이다.

따라서 어떤 클래스가 bar 라는 프로퍼티를 가지고 있다는 것은 그 프로퍼티가 -bar 혹은 -setBar: 라는 접근자 메소드를 가지고 있음을 의미한다. 그리고 그 객체에서 해당 프로퍼티를 액세스하는 것은 해당 접근자 메소드를 호출해야 하는 일이고, 따라서 객체로부터 어떤 값을 얻어와서 사용한다는 것은 “하드 코딩된 코드에서 미리 정해진 접근자 메소드를” 사용해야 한다는 것이다.

키밸류 코딩 – 문자열 기반 이름으로 동적인 프로퍼티 액세스

그런데, 임의의 객체 인스턴스 zoo 가 있다고 하자. 이 객체로부터 어떤 프로퍼티를 액세스해서 그 값을 얻으려고 한다. 그런데 어떤 프로퍼티를 가져올 것인지 혹은 갱신할 것인지가 컴파일 타임에 결정되지 않는다면 어떻게 해야 할까? 즉 “어떤 접근자 메소드를 호출할 것인지”를 코드를 작성하는 시점에 알 수 없는 것이다. “pee”라는 이름의 프로퍼티일 수도 있고, “tee”라는 이름의 프로퍼티 일수도 있는 것이다. 물론 객체 zoo가 이러한 접근자 메소드를 갖고 있는지 아닌지 여부조차 알 수 없을 수도 있다.

이처럼 컴파일 타임에 정의되지 않은 접근자 이름을 사용해서 런타임에 특정한 이름의 프로퍼티에 접근할 수 있는 기술이 키밸류 코딩이다. 키 밸류 코딩은 간단히 다음의 네 개의 메소드에 의존한다.

  • - (id)valueForKey:(NSString*)key / -(id)valueForKeyPath:(NSString*)keyPath
  • - (void)setValue:(id)obj forKey:(NSString*)key / - (void)setValue:(id)obj forKeyPath:(NSString*)keyPath

이 메소드들은 NSObject에 의해서 이미 구현되어 있다. 이 메소드들을 호출하여 성공적으로 특정한 프로퍼티에 액세스하기 위해서는 처음에 프로퍼티를 정의할 때, ivar와 접근자 메소드들의 이름이 중요하다.

  • valueForKey: 에서 키이름이 getter 메소드와 같거나
  • 키 이름과 동일한 ivar 혹은 앞에 언더스코어가 붙은 키 이름의 ivar가 있다.
  • setValue:forKey:는 키 이름을 첫글자를 대문자로 바꾸고 그 앞에 set-을 붙인 setter 메소드가 있다.

이러한 가정을 두고 있는 것이다. 만약 [zoo getValueForKey:@"bar"] 라고 했을 때,   zoo 가 Foo의 인스턴스라면, 이 메시지는 Objective-C 런타임 내부에서 [zoo bar] 로 번역될 것이다. 그리고 [zoo setValue:@"hello" forKey:@"bar"]라는 메시지를 받는다면 이는 다시 [zoo setBar:@"hello"];로 변경되어 호출될 것이다.1

키밸류 코딩을 따르는 방법

키밸류 코딩 호환 클래스를 작성하는 방법은 간단하다. 키밸류 코딩은 결국 키 이름을 기반으로 그에 매칭되는 접근자 메소드 및 인스턴스 변수를 런타임이 동적으로 찾아서 액세스해주는 기술이기 때문에 어떤식으로 프로퍼티 이름을 짓느냐는 것만, 관습을 따르면 되며, 그 관습이란 앞서 소개한 Foo의 bar와 같다.

  • 기본적으로 getter 이름이 프로퍼티 이름이며, 이것이 곧 키 이다.
  • setter 이름은 setKeyName: 과 같은 식으로 작성한다. getter이름의 첫글자를 대문자로 바꾸고 앞에 set을 붙인다.
  • ivar 이름은 getter이름과 똑같거나, 앞에 언더스코어를 붙인다.

그리고 이 관습은 @property 문법을 쓰면 자동으로 지켜진다.

@interface Foo: NSObject
@property (copy, nonatomic) NSString* bar;
@end

이상의 코드만으로 키밸류 코딩에서 요구하는 인스턴스변수, getter 메소드, setter 메소드를 모두 작성한 것과 다름없는 결과를 얻을 수 있다. 이것은 언어의 기능이라기보다는 컴파일러가 소스코드를 처리하기 직전에 자동으로 관련 코드를 만들어서 삽입해준다고 보면 된다. (이전에는 @synthesize bar; 같은 구문을 구현부에 써야했는데, LLVM 컴파일러는 이런 처리도 모두 자동으로 해주기 때문에 굳이 쓸 필요없다.)

키밸류 코딩은 왜 중요한가

그렇다면 키밸류 코딩은 왜 중요한가? 이것은 특정한 프로퍼티가 변경될 때, 자동으로 옵저버들에게 통지가 가는 키밸류 옵저빙을 비롯하여, 이 기술을 기반으로 하고 있는 코코아 바인딩등에서 기본 가정으로 “모든 참여 객체가 KVC/KVO 호환이다”라는 것을 가정하기 때문이다.

  • 키밸류 코딩 이름 규칙을 지원하면 valueForKey:, setValueForKey:는 따로 구현하지 않더라도 자동으로 지원된다.
  • 키밸류 코딩 규칙을 따르더라도 _bar = @"hello";와 같이 인스턴스 변수를 직접 변경해버리면 이는 KVO와 호환되지 않는다.
  • KVO에서는 반드시 [foo setValue:@"hello" forKey:@"bar"]를 쓰지 않아도 된다. [foo setBar:@hello]라고만 써도, 런타임에서 자동으로 통지를 보낼 수 있다. self.bar = @"hello"; 역시 setter 메소드 호출과 1:1로 치환되므로 KVO 호환이 된다. 이는 KVC 호환인 메소드는 필요한 경우 런타임에 의해 자동으로 다른 내부 메소드로 치환되기 때문에 적용가능하다. 물론 메소드 이름이 정해진 규칙을 벗어나면 이러한 기능은 지원되지 않는다.

기본적인 키밸류 코딩은 특정한 단일 값 프로퍼티의 변경을 런타임에서 동적으로 관리하는 수준에서 적용된다. 하지만 Foundation에서는 배열이나 Set과 같은 집합형식 자료 구조에 대해서도 KVC/KVO를 지원한다. 이는 단순히 이름 규칙만으로는 지원될 수 없으며, 별도의 메소드들을 추가로 작성해주어야 하는데 (대부분 NSMutableArray, NSMutableSet의 메소드들 간단힌 래핑하는 수준의 구현이다.) 이를 지원하도록 하는 것은 다음 기회에 추가로 소개하도록 하겠다.

참고자료

 

 

 

 

 


  1.   물론 키밸류 코딩은 이렇게 간단한 일차원적 변환 이상의 것이다. 실질적으로 @property 문법이 확립되어 적용되기 이전부터 존재해온 기술이기 때문에 탐색 패턴은 좀 더 많은 경우를 순차적으로 따르게 된다.