[Objective-C] Objective-C의 메모리 관리 방법

Objective-C의 메모리 관리

메모리를 할당하고 해제하지 않는다면 프로그램이 이런 작업을 반복하면 할 수록 계속해서 새로운 메모리를 사용하게 되고, 종국에 가서는 더 이상 쓸 수 있는 메모리가 남아나지 않는 상황이 벌어질 수 있다. 그리고 최근의 OS들의 그 복잡한 구조 때문에 이렇게 잘못 만들어진 프로그램이 아닌 다른 프로그램이 크래쉬될 수 있는 위험도 있다. C 계열 언어에서의 메모리 관리란 매우 중요하면서도 프로그래머의 골치를 썩히는 문제이다.

C의 메모리 관리 규칙

Objective-C는 C의 상위집합으로 기본적으로는 C의 메모리 관리 규칙을 따른다. C의 메모리 관리 규칙은 다음과 같다.

  1. *alloc으로 할당한 메모리는 반드시 free 해야 한다. 코드 전체에 대해 alloc 과 free의 짝이 맞아야 한다.
  2. alloc을 수행한 함수가 free의 책임을 져야 한다.
  3. 2가 어려운 경우에는 (새로 할당한 메모리의 주소를 리턴하는 함수 등) 이를 호출하는 쪽에서 free의 책임을 져야 한다.

특히 세 번째 경우가 난감해진다. 예를 들어 문자열을 전달받아 이를 대문자로 만든 문자열을 리턴하는 함수를 생각해보자.

char * upperString(const char *str)
{
    char *upperStr = (char *)malloc(strlen(str) + 1);
    char *i = str;
    int offset = 0;
    while(*i != '\0')
    {
        *(upperStr + offset++) = toupper(*i++);
    }
    *(upperStr + offset) = '\0';
    return upperStr;
}

이 함수는 전달받은 문자열 전체를 대문자로 변경한 문자열을 반환한다. 함수 내에서 alloc을 하고 있는데, 이 주소를 다시 리턴해야 하므로 그 이전에는 free 할 수 없다. 따라서 이 함수를 사용하는 경우에는 메모리의 해제 책임은 이 함수를 호출하는 쪽에서 해야 한다.

char *original = "This is a book.";
*char upStr = upperStr(original);
printf("%s", upStr);
free(upStr);

하지만 이렇게 함수 내에서 메모리를 할당하는 경우에는 할당과 해제의 짝을 맞추기가 어렵다. alloc – free의 짝이 아닌 이런 함수의 호출을 모두 체크해야 하기 때문이다. 따라서 표준 C 함수들은 이렇게 함수 내부에서 할당하지 않고 바깥에서 이미 할당한 포인터를 받는 식으로 처리한다. 문자열의 사본을 만들어주는 strcpy의 경우를 생각해 보자.

void strcpy(char *str1, const char* str2, size_t size);

마찬가지로 우리는 위에서 작성한 upperString 함수의 디자인을 이런 식으로 변경할 필요가 있다.

void upperString(char *upperStr, const char *str, size_t size)
{
    size_t offset = 0;
    while (offset < size)
    {
        *(upperStr + offset) = toupper(*(str+offset));
        offset++;
    }
    *(upperStr + offset) = '\0';
}

이 함수를 사용하기 위해서는 함수의 외부, 즉 호출하는 부분에서 alloc으로 필요한 크기의 메모리를 할당하고 이곳에 문자열의 사본을 복사한다. 나중에 free는 역시나 이 함수의 호출부에서 책임지면 된다.

Objective-C의 메모리 관리 규칙

Objective-C의 객체도 마찬가지로 메모리 누수가 생기지 않도록 alloc 한 객체는 반드시 dealloc 해줘야 한다. 하지만 코드 내에서 객체가 다른 함수로 넘겨지거나 하는 일이 상당히 빈번하므로 일일이 dealloc 하는 것은 너무나 복잡한 문제이고 나중에 메모리 누수가 발견되면 문제가 되는 부분을 찾는 것도 만만치 않게 복잡해 진다. 그래서 Objective-C 에서는 참조수 (refernce count 혹은 retain count)라는 것을 도입하게 된다.

클래스에 alloc 메시지를 보내면 그 객체를 위한 메모리 공간이 확보되고, 객체가 만들어진다. (실제로 alloc 하는 시점에 모든 인스턴스 변수는 0이나 nil로 채워지게 된다.) 그리고 해당 객체가 필요없는 시점이 오면 Objc 런타임은 해당 객체에게 자동으로 dealloc 메시지를 보내어 객체를 파괴하게 된다. 이에 입각하여 메모리 관리 규칙을 따져보도록 하자.

  1. new, alloc, copy로 객체를 생성하는 경우 이 객체는 해당 시점에 1의 참조수(retain count)를 가지게 된다.
  2. 객체를 쓰고 더 이상 필요 없는 시점이라면 release 메시지를 보낸다. 이는 객체의 참조수를 1만큼 감소 시킨다. 따라서 어떤 함수 내에서 alloc 으로 생성한 객체에 release메시지를 보내면 해당 객체의 참조수는 0이되고, 런타임은 해당 객체를 파괴한다.
  3. 함수 내에서 객체를 생성해서 반환해야 하는 경우에는, 리턴하기 전에 생성한 객체에 autorelease 메시지를 보내어, 오토릴리즈 풀이 해제될 때 객체가 release 될 수 있도록 한다.
  4. 만약 1에서 언급한 방식이 아닌 다른 방삭으로 획득한 객체가 있다면 (다른 함수의 결과로 리턴된 객체 포인터) 객체를 획득하는 시점에서의 참조수는 1로 보고, 이 객체는 autorelease된다고 보면 된다.

ObjC에서는 객체를 생성하여 리턴하는 메소드를 상당히 빈번하게 작성하게 된다. 이런 경우에 표준 C의 함수와 같은 식으로 디자인하는 것은 상당히 까다롭다. 결국 맨 처음에 만든 것과 같이 함수 내에서 객체를 새로이 생성하여 리턴하는 식의 코드를 자주 작성하게 된다. 또한 Foundation 프레임워크 내에서도 이런식으로 객체를 만들어서 반환하는 메소드들이 매우 많으므로 함수 내에서 생성된 객체를 받는 케이스가 많다. 그래서 3, 4의 규칙이 추가된다고 보면 된다. 즉 기본적으로 객체를 신규로 생성한 객체 (혹은 함수)는 해당 객체를 release 해야 하는 책임을 진다. (이 때 이 객체 / 함수를 ‘소유주’로 본다.) 만약 어떤 함수나 객체가 신규로 생성한 객체를 리턴해야 하는 상황이라면 이 리턴 되는 객체가 계속 남지 않도록 하기 위해서는 오토릴리즈 객체를 리턴하도록 하면, 프로그램의 특정 시점에서 풀이 해제될 때 이 객체가 release 메시지를 받을 수 있다.

짧은 시간동안 생성해서 쓰는 임시 객체

만약 함수 내에서 일시적으로 생성했다가 쓰는 객체가 있는 경우에는 다음과 같은 식으로 처리하면 된다.

SomeObject *anObj = [[SomeObject alloc] init];
// anObj를 가지고 작업 수행
[anObj release];

anObj는 alloc 되는 시점에 retain count가 1이된다. 그리고 처리를 마친 후 release 메시지를 받게되면 참조수가 0이 되고, 런타임은 적절하게 dealloc 메시지를 보내어 해당 객체를 파괴하고 메모리 자원을 돌려받게 된다.

오토릴리즈

실제 코딩을 할 때는 함수 내에서 새로운 객체를 메모리를 할당해 생성하고 이를 리턴하는 경우가 많은데, 코딩하는 시점에 일일이 이를 적절히 해제하기란 어렵다. 또한 실행중에 ‘객체가 더 이상 필요없음’을 정하기도 만만치 않은 문제이다. 이에 Foudation에서는 오토릴리즈 풀이라는 기능을 사용하여 함수 내에서 생성한 객체들을 적절한 시점에 한 번에 해제할 수 있도록 해주는 방법을 제공한다.

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSMutableArray *arry = [NSMutableArray arrayWithCapacity:30];
// array를 사용한 작업
[pool drain];

많은 팩토리 메소드들 (alloc을 사용하지 않고 array.., string… 등으로 이름이 시작하며, 객체를 반환하는 메소드들)은 반환시에 생성한 객체에 autorelease 메시지를 보내 이를 오토릴리즈 풀이 등록하게 된다. 이런 오토릴리즈 객체들은 나중에 풀이 해제되거나 [pool drain]을 만나게 되면 모두 relase 메시지를 받게 된다. (물론 메모리 관리 규칙에 알맞게 처리했을 경우이다. 풀은 release를 한 번만 보내기 때문에, retain count가 2 이상인 객체는 여전히 살아남을 수 있다.)

NSMuatbleArray의 array…로 시작하는 메소드들이 대부분, 이러한 오토릴리즈 객체를 반환하게 된다. 따라서 사용하는 입장에서는 이들 객체가 참조수 1을 가지며, autorelease되는 객체라고 보면 된다. 따라서 필요없는 시점에서는 참조수가 1이 되도록, 만약 수동으로 retain 했다면 그 횟수만큼만 release 하면 된다.

객체의 프로퍼티

어떤 객체가 다른 객체에 대한 포인터를 저장하는 인스턴스 변수를 가지고 있을 때, 참조수는 조금 더 주의깊게 관리되어야 한다. child라는 다른 객체를 사용하는 프로퍼티가 있을 때 이 프로퍼티에 대한 setter는 다음과 같이 생각할 수 있다.

-(void)setChild:(id)child
{
    _child = child; // _child는 인스턴스 변수
}

이는 위험한 상황을 야기한다. 만약 다른 곳에서 만들어진 child가 세팅되었다가 다른 어딘가에서 해제된 후 dealloc 되었다면 후에 이 객체의 child에 접근하는 것은 해제된 곳의 메모리를 참조하기 때문에 앱을 다운시키게 된다. 따라서 객체의 프로퍼티에 다른 객체를 지정할 때는 최소한 이 객체가 릴리즈 되는 시점까지는 이를 붙잡아 두어야 한다. 따라서 다음과 같이 수정할 필요가 있다. (! 이 방법도 사실 틀렸다.)

-(void)setChild:(id)child
{
    [child retain];
    _child = child;
}

이렇게하면 child로 지정한 객체가 외부에서 release를 받더라도 참조수는 최소한 1을 유지하기 때문에 파괴되지 않을 것이다. 문제는 이 객체의 child를 다른 객체로 바꾸는 경우에는 기존의 child가 release되지 않기 때문에 계속해서 남게 된다. 또한 다른 곳에서 release를 하더라도 참조수가 없어지지 않으므로 한번 child가 된 객체들은 메모리에 여전히 살아있게 된다.

-(void)setChild:(id)child
{
    [_child release];
    [child retain];
    _child = child;
}

그런데, 이 코드도 틀렸다! 왜? 만약 같은 객체포인터를 다시 대입한다고 하면 release 시점에 가지고 있던 child는 파괴되고, 이 파괴된 객체를 참조하여 대입하려하니 앱이 곧장 죽을 것이다. 안전한 코드는 최종적으로 다음과 같다.

-(void)setChild:(id)child
{
    [child retain];
    [_child release];
    _child = child;
}

그리고 이런 식으로 세팅 시에 retain을 해준 프로퍼티가 있다면, 객체의 해제 시점에 모두 해제해야 한다. 이 객체의 dealloc 은 다음과 같은 모양이어야 한다.

-(void)dealloc
{
    [[self child] release];
    [[self anotherChild] release];
    [super dealloc];
}

객체의 부모 클래스가 따로 선언해둔 인스턴스 변수가 있다면, 이는 부모 클래스의 dealloc 에서 해제할 것이므로 [super dealloc]을 호출해주어야 한다. (이마저도 ARC 환경에서는 생략해야 하지만.)

상호참조의 경우

만약 어떤 함수 내에서 객체 A를 생성했고, 이 객체는 init 되면서 child라는 인스턴스 변수에 다른 객체 B를 생성한다고 가정해보자. 이 때 메모리에서 참조를 하는 상태는 다음 도식과 비슷해질 것이다.

{함수} ====> A ====> B // A:1, B:1

각각 alloc 을 통해 생성되었다면 A의 참조수는 1이고 B의 참조수도 1이다. A는 B를 생성하여 인스턴스 변수로 가지고 있는데 이는 소유관계이며, B를 해제할 책임은 A에게 있다. 따라서 함수가 A를 해제할 때 A가 파괴되기 전에 B를 해제해야 한다. 이런 경우를 대비해서 우리는 A의 dealloc 메소드를 적절하게 오버라이드 할 수 있다.(!그렇지만 우리는 A의 dealloc을 직접 호출해서는 안된다.)

-(id)init
{
    self = [super init];
    if(self)
    {
        _child = [[ObjectChild alloc] init];
    }
    return self;
}

-(void)dealloc
{
    [_child release];
    [super dealloc];
}

참 쉽다. 객체를 초기화할 때 자식 객체를 생성했으므로, dealloc 시점에서 이 객체를 릴리즈 한다. 여기까지는 별 문제가 아닌데 만약 자식 객체가 부모 객체에 대한 참조를 한다고 생각해보자.

//child 객체의 `-setParent`
-(void)setParent:(id)parent
{
    [parent retain];
    [_parent release];
    _parent = parent;
}

그럼 위의 도식은 다음과 같이 변경된다.

{함수} ====> A <====> B // A:2, B:1

여기서 A를 릴리즈하면 다음과 같은 일이 벌어진다.

{함수} X A <====> B // A:1, B:1

즉 함수에서 [A release]를 하게 되면, A의 참조수는 1이 되므로 dealloc 메소드가 호출되지 않는다. 그리고 A와 B는 서로가 서로에 대한 참조를 유지하고 있으므로, 여전히 메모리 상에 짝지어 남아있게 된다.

이런 문제를 해결하기 위해서 약한 참조를 사용해야 한다. 약한 참조는 retain count를 증가시키지 않고 객체의 포인터만을 가지고 있는 경우이다.

{함수} ====> A =====> B
(A <—– B) // A:1 , B:1

이 때는 A를 릴리즈하면 참조수가 0이되면서 dealloc 을 통해 B까지 릴리즈 할 수 있다.

여기서 다시 문제. 만약 다른 제 3의 객체가 개입한다면 어떨까? 만약 제 3의 객체 C가 B를 참조한다면 B의 참조수는 2가 될 것이다. 이 때 A를 릴리즈하면 A는 파괴되면서 B를 릴리즈하지만 C가 B를 릴리즈 하기 전까지는 B가 유지된다. 만약 C가 B를 약한 참조로 참조한다면? A가 릴리즈 되는 시점에 B도 릴리즈 되면서 B가 파괴된다. 여전히 C는 B를 참조하고 있으므로 C를 통해 B를 참조하는 경우에 앱이 다운될 것이다.

하지만 다행스럽게도 이런 참사는 일어나지 않는다. 만약 C가 B를 약한 참조로 참조하고 있는데, B가 파괴된다면, C의 B에 대한 참조는 nil로의 참조로 자동으로 바뀌게된다. 이를 zeroing weak reference라고 하며 iOS5나 OSX10.7 SDK에서부터 지원한다. (그렇다 상당히 최근에 추가된 기능이다.)

Gabage Collection

메모리 누수를 막아내기 위해 C프로그래머들이 고군분투하는 동안, 우리의 친구들인 Java 프로그래머들은 이러한 메모리 누수 문제에 있어서 비교적 자유롭다. 자바에서는 GC(Garbage Collect)라는 것이 있다. 이 GC는 실행 중 적절한 시점에 프로그램 내의 객체들을 검사하여 외부에서 이 객체에 대한 참조가 없는 경우 이를 쓰레기로 판단, 자동으로 파괴해 버린다. 따라서 자바 프로그래머는 객체가 더이상 필요없는 시점이 되면 해당 객체의 포인터에 NULL을 대입해주기만 하면 된다. 그러면 tangling 된 객체는 CG가 알아서 제거해준다.

왜 이런 이야기를 하냐면 Objective-C에서도 이런 GC 기능이 있지롱~ 이는 Xcode의 빌드 세팅에서 GC 서포트 옵션을 켜주고 컴파일하면 된다. 문제는 역시나 위에서 언급한 상호참조처럼 두 개의 객체가 다른 곳으로의 참조는 제거된 채 둘끼리만 참조하고 있는 경우인데, GC는 제법 똑똑하기 때문에 이러한 객체들도 제거할 수 있다. (항상 그런 건 아니다.)

문제는 GC는 상당히 비용이 많이 드는 작업이라는 거다. 시간도 그렇고 시스템이 적극적으로 모든 객체를 검사하기 때문에 경우에 따라서는 앱 사용 중 메모리가 부족한 상황이 오고, GC가 발동하면 앱이 잠시 멈추는 것 같은 상황이 올 수 있다. 최근의 자바 런타임은 GCD를 이용해서 보다 부드럽게 병렬로 이 작업을 수행할 수 있다고 하지만 이는 일부 PC급 플랫폼에서의 이야기이고, 자원이 굉장히 제한되어 있는 엠베드 환경에서는 여전히… 그리고 결정적으로 iOS에서는 이 GC를 사용할 수 없다.

ARC

그렇다고 해서 아예 방법이 없는 건 아니다. 애플은 Objective-C로 만든 앱의 성능을 향상시키는 동시에 작업 과정에서의 많은 편의성을 제공하기 위해서 llvm 이라는 차세대 컴파일러를 도입했다. (Xcode 4로 넘어오면서 바꾼 걸로 알고 있다.) llvm은 gcc보다도 자세한 오류 보고를 해주며 (예를 들어 소스의 어느 부분에 오타가 있는지, 포맷문자와 그에 대응하는 변수 타입이 다른지, 사용하지 않는 변수를 선언만하고 쓰지 않고 있는지 등) Xcode는 이에 힘입어 실시간 자동완성, 더 빠른 컴파일, 디버깅 시에 제공되는 더 강력하고 편리한 기능들을 구현하고 있다. ARC는 llvm의 이러한 강력한 기능을 바탕으로 좀 더 똑똑해진 컴파일러가 프로그래머의 고충을 덜어주기 위해 개발된 기능이다. ARC는 Automatic Reference Counting의 약자로, 앞서 설명한 ‘참조수’에 대한 문제를 좀 더 쉽게 풀어줄 수 있게 한다.

어떻게? 바로 retain과 release하는 코드를 컴파일러가 컴파일 시점에 소스 곳곳에 적절하게 추가해준다. 개발자는 강한 참조든 약한 참조든 신경쓰지 않고 그냥 ‘참조’만 하면 된다. 응? 이게 말이돼? 말이 된다… 실제로 하고 있고, 이미 많은 iOS앱들이 ARC 환경에서 컴파일 되어 동작하고 있다. 그리고 ARC는 컴파일된 앱의 실행 퍼포먼스에서는 전혀 영향을 미치지 않는다. ARC로 컴파일하면 “고도로 잘 신경써서 수작업으로 메모리 관리 코드를 작성한” 상태로 만들어 컴파일하기 때문이다.

ARC 적용

ARC는 이름에서 볼 수 있듯이 Retain Count를 자동으로 관리해준다. 따라서 이 방식으로 관리 가능한 객체의 메모리를 런타임이 알아서 관리할 수 있도록 도와주는 수단이다. ARC의 적용을 받기 위해서는 참조수로 관리되는 객체들만 대상이 되는데 이러한 것들은 다음과 같다.

  • block 객체
  • Objective-C 객체
  • attribute((NSObject))로 선언된 typedef 사용자 정의타입

C의 일반 T타입포인터, 구조체 포인터 및 코어파운데이션 객체는 여전히 ARC의 적용을 받지 않는다. 이러한 것들을 사용할 때는 프로그래머가 메모리의 할당과 해제를 수동으로 처리해야 한다. (free, CFRelease 를 사용)

ARC 규칙

다음은 ARC를 적용하기 위한 코딩 상의 규칙이다.

  1. 명시적으로 dealloc을 호출해서는 안된다.
  2. retain, release, retainCount, autorelease를 명시적으로 호출하거나 오버라이드 할 수 없다.
  3. dealloc 메소드에 대해 ARC의 적용을 받지 않는 인스턴스 변수를 해제하기 위해 오버라이드를 할 수는 있다. 단 이 때에도 [super dealloc]을 명시적으로 호출해서는 안된다.
  4. CFRetain, CFRelease는 앞서 설명했듯이 사용할 수 있다. 코어 파운데이션 타입은 ARC에서 제외되므로 수동으로 관리해야 한다.
  5. C구조체 내에 Objective-C 객체 포인터를 저장할 수 없다. 구조체보다는 Objective-C 객체를 쓸 것을 추천한다.
  6. id 와 void *간의 캐주얼 캐스팅을 쓸 수 없다. 이들 간의 캐스팅에서는 컴파일러가 객체의 라이프사이클을 파악할 수 있도록 추가적인 지시어 (__reatin 등)를 써서 관계를 명시해야 한다.
  7. NSAutoreleasePool 객체는 더 이상 쓸 수 없다. @autoreleasepool{ } 블럭을 사용한다.
  8. NSZone을 쓸 수 없다. 어차피 ARC가 아니어도 최신 런타임은 이를 무시해버린다.
  9. 접근자명에 new를 붙일 수 없다. @property NSString *newTitle;은 컴파일 오류를 일으킨다.
  10. 단, getter 명을 바꾸면 쓸 수는 있다. @property (getter=theNewTitle) NSString *newTitle;은 동작한다.

새로운 프로퍼티 지정자

ARC 이전의 프로퍼티 지정자는 retain, assign 같은 것들이 있었는데 이들은 보다 명시적으로 참조의 종류를 가리키도록 strong, weak로 변경된다. strong 속성을 부여한 프로퍼티는 세팅시에 참조수를 올려 외부에서 릴리즈되더라도 해당 객체가 소멸될 때까지 참조를 유지한다. weak 속성으로 선언된 프로퍼티는 참조수를 올려 유지하지 않는대신, 외부에서 가리키고 있던 객체가 해제된 경우 자동으로 nil을 가리키도록 변경된다. ARC의 디폴트 지정자는 strong 이다.

변수 지정자

프로퍼티 지정자처럼 변수 지정자가 추가되었다.
strong
weak
unsafe_unretained
autoreleasing
__strong은 디폴트 값이다. 이 변수에 할당한 객체는 강한 참조를 하게 된다.
weak는 약한 참조만을 갖도록 한다. 변수가 가리키는 객체가 파괴되면 (객체는 강한참조의 수가 0일 때 자동으로 파괴된다) nil로 변경된다.
*
unsafe_unretained 는 강한 참조처럼 객체를 유지하지 않지만 약참조처럼 nil로 변경되지 않는다. CF객체나 C포인터등을 가리킬 때 사용한다.
* __autoreleasing은 자동 해제될 객체를 담는 변수이다. 함수의 인자로 넘겨지는 변수는 모두 이 타입을 사용한다.

예를 들어 __weak를 쓰는 다음 코드를 보면

NSString * __weak string = [[NSString alloc] initWithFormat:@"First Name : %@", [self firstName]];
NSLog(@"%@", string);

string 객체가 가리키는 대상 문자열은 string에 의해 약한 참조를 받게 된다. 객체에 대한 강한 참조가 없으므로 이는 런타임에 의해 즉시 해제된다. 따라서 다음 라인에서 콘솔에 찍히는 문자열은 nil이될 것이다. 컴파일러는 통상 이런 경우에 경고를 남겨줄 것이다.

__autoreleasing 지시어는 함수에 넘겨지는 타입이다. 흔히 에러 캐치를 위해 에러 객체의 참조를 함수로 넘기는 경우에 다음과 같은 코드를 볼 수 있다.

NSError *error;
BOOL OK = [myObject performOperationWithError:&amp;error];
if(!OK)
{
    // Error Handling.
}

객체 변수에 대한 묵시적인 선언은 모두 __strong이므로, 컴파일러는 위 코드를 다음과 같이 고치게 된다.

NSError *error;
NSError * __autoreleasing tmp;
temp = error
BOOK OK = [myObject performOperationWithError:&amp;temp];
error = tmp;
if(!OK)
{
    // Error Handling...
}

메모리 문제를 방지하는 코딩 습관

객체 내부에서 인스턴스 변수에 접근할 때도, 변수보다는 접근자를 쓸 것

ARC를 사용하지 않는 경우에 객체의 참조수 관리를 명확하게 하기 위해서 객체인 인스턴스 변수를 참조하거나 세팅하기 위해서는 접근자를 쓰는 것이 좋다. 이 때 접근자는 앞서 설명한 방식을 사용하도록 한다.

만약 객체내에서 카운트(NSNumber)를 초기화하는 동작을 수행하고자 할 때는 다음과 같은 코드를 쓴다.

-(void)reset
{
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [self setCount:zero];
    [zero release];
}

물론 자동 해제되는 객체를 위한 numberWithInteger를 쓸 수도 있다.

-(void)reset
{
    NSNumber *zero = [NSNumber numberWithInteger:0];
    [self setCount:zero];
}

하지만 다음과 같은 코드는 대략 좋지 않다.

-(void)reset
{
    NSNumber *zero = [[NSNumber alloc] initWithInteger:0];
    [_count release];
    _count = zero;
}

이와 같은 접근은 어느 시점에선가 실수를 부를 수 있다.

init, dealloc에서는 접근자를 쓰지 말 것.

일반적으로 접근자를 통한 인스턴스 변수 세팅은 참조수를 변경하는 것과 관련이 있으므로, 이 들 메소드내에서는 곧장 인스턴스 변수를 쓰도록 한다.

-(id)init
{
    self = [super init];
    if(self)
    {
        _count = [[NSNumber alloc] initWithInteger:0];
    }
    return self;
}

혹은

-(id)initWithCount:(NSNumber*)startingCount
{
    self = [super init];
    if(self)
    {
        _count = [startingCount copy];
    }
    return self;
}

순환참조를 피하도록 약한 참조를 사용할 것

사용 중인 객체가 해제되지 않도록 할 것

사용중인 객체가 사용중에 해제되어 버리지 않도록 retain 해야 할 필요가 있을 때 retain 하고, 사용후에는 release 해야 한다.

NSNumber *someNumber = [numberArray objectAtIndex:11];
[numberArray removeObjectAt:11];
// someNumber는 유효하지 않은 주소를 가리키게 된다. 

이는 다음과 같이 써야 안전하다.

NSNumber *someNumber = [[numberArray objectAtIndex:11] retain];
[numberArray removeObjectAt:11];
// someNumber를 사용한다.
[someNumber release];

수동으로 dealloc을 호출하지 말 것

dealloc 되어야 하는 적절한 시점이 오더라도 수동으로 어떤 객체에게 dealloc 메시지를 보내어선 안된다. 런타임은 필요없는 (참조수가 0인) 객체에 자동으로 dealloc 메시지를 보내는데, 경우에 따라서는 일정 시간 동안 지연할 수도 있기 때문이다.

집합과 참조수

집합(Collection : NSArray, NSDictionary, NSSet 등)에 객체를 추가하면 집합은 추가된 객체에 대한 소유권을 가지게 된다. 즉 add 하는 시점에 retain 하게 된다. 그리고 remove 하게되면 remove 하는 시점에 해당 객체에게 release를 보내게된다. 따라서 만약 alloc, new, copy 가 아닌 다른 방법으로 획득한 객체를 집합에 추가하면 명시적으로 release 할 필요는 없다.

  • ㅇㅇㅇ

    나같은 풋내기도 쏙쏙 이해가 잘 되는 좋은 글에 감사올립니다.

    • 저도 아직 풋내기라… 눈높이가 맞은 듯 합니다. 도움이 되셨다니 제가 감사하지요.

  • 지금 메모리 성능에 대한 이슈가 있는데 좋은글 감사합니다.