ProgrammingInObjectiveC:Chapter 17
- 17장
- 메모리 관리
17장 :: 메모리 관리
지금까지 이 책에서는 메모리 관리에 초점을 맞추어 왔다. 이제 객체를 릴리스 해야 할 때와, 릴리스 해서는 안 될 때를 구분할 수 있을 것이다. 여태 공부한 예제들은 비록 짧고 간단했지만, 좋은 프로그래밍 습관을 알려 줬고, 메모리 누수 없는 프로그램을 개발하도록 메모리 관리의 중요성을 강조해 가르쳤다.
어떤 응용 프로그램을 작성하느냐에 따라, 메모리를 반드시 신중하게 써야만 할 때도 있다. 예를들어, 개발하고 있는 인터랙티브 드로잉 응용 프로그램에서 프로그램이 실행되는 동안 다양한 객체들을 생성한다고 해보자. 프로그램이 실행되면서 메모리를 점점 더 소비하지 않도록 주의를 기울여야만 한다. 이때, 리소스를 지능적으로 관리하여 더는 필요없을때 메모리에서 해제시켜야 한다. 이 말은, 프로그램이 끝날 때까지 기다리지 않고 프로그램이 실행되는 도중에 리소스를 해제 해준다는 의미다.
이 장에서 Foundation 이 메모리를 어떻게 관리하는지 그 전략에 대해서 더 상세히 배운다. 여기에는 오토릴리스 풀과 객체의 리테 인 개념을 더 철저히 이해하는 과정이 필요하다. 또한, 객체의 레퍼런스 카운트에 대해서도 배울 것이다. 마지막으로, 객체를 리테인하고 사용하고나서 릴리스해 줘야 하는 부담을 덜고자 '가비지 컬렉션' 기법에 대해 배운다. 그러나 곧 알겠지만, 아이폰 응용 프로그램에서는 가비지 컬렉션을 사용할수없다. 그러므로 결국 이 책의 전반에 걸쳐 설명하는(그리고 이 장에서 더 상세히 설명하는) 메모리 관리 기법을 이해해야 한다.
오토릴리스 풀
이미 2부 예제들을 살펴보며 오토릴리스 풀에 익숙해졌을 것이다. Foundation 프로그램을 다룰 때는 오토릴리스 풀을 설정해서 Foundation 객체를 사용해야 한다. 시스템은 차후에 릴리스할 객체에 대한 정보를 이 풀에서 얻어 온다. 이미 보았듯이 다음과같이 호출하여 풀을 설정할수 있다.
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
풀을 설정하고 나면 Foundation 은 자동으로 여기에 특정 배열, 스트링, 딕셔너리와 다른 객체들을 추가한다. 이 풀을 다 쓰고 나면 drain 메시지를 보내 풀이 사용하는 메모리를 릴리스한다.
[pool drain];
오토릴리스 풀이라는 이름은, 어느 객체든 오토릴리스라고 표시되기만 하면 풀에 추가되고, 후에 풀이 릴리스될 때 객체도 함께 릴리스된다는 점에서 붙여졌다. 사실, 여러분의 프로그램도 오토릴리스풀을 하나 이상 가질수 있으며, 오토릴리스 풀을 중첩해 쓸 수도 있다.
만일 프로그램에서 임시 객체를 많이 생성한다면(반복문 안에서 코드를 실행한다면 쉽게 나타나는 현상이다), 오토릴리스 풀을 여러 개 생성해야 할 수도 있다. 예를 들어, 다음 코드 조각을 보자. 여기서는 반복문을 돌 때마다 생성되는 임시 객체들을 릴리스해 줄 오토릴리스 풀을 생성하는 방법을 보여 준다.
NSAutoreleasePool *tempPool;
...
for (i = 0; i < n; ++i) {
tempPool = [[NSAutoReleasePool alloc] init];
... // 이곳에서 임시 객체를 다루는 많은 작업을 수행한다.
[tempPool drain];
}
오토릴리스 풀 자체는 실제로 객체를 보유하지 않는다는 점에 유의하자. 풀이 빠질때, 릴리스될 객체의 레퍼런스만 담는다.[1]
현재 오토릴리스 풀에 나중에 릴리스할 객체를 추가하려면 autorelease 메시지를 객체에 보내면 된다.
[myFraction autorelease];
시스템은 나중에 릴리스하기 위해 myFraction 을 오토릴리스 풀에 더한다. 곧 보겠지만, autorelease 메서드는 메서드에서 나중에 버릴 객체를 표시할 때도 유용하다.
레퍼런스 카운트
기본 Objective-C 객체 클래스인 NSObject 에 대해 이야기했을 때, 메모리는 alloc 메서드로 할당되고, 그 뒤에 release 메시지로 해제할 수 있다고 설명하였다. 불행히도, 언제나 이렇게 간단하지는않다. 실행 중인 프로그램은 여러 곳에 생성된 객체들을 참조할수 있다. 예를 들어, 객체는 배열에 저장될 수도 있고, 다른 곳에 있는 인스턴스 변수에 의해 참조될 수도 었다. 모든 이가 그 객체를 다 쓴 뒤라는 것을 확실히 알기 전에는 객체가 사용하는 메모리를 해제할 수 없다.
다행스럽게도, Foundation 프레임워크는 객체를 참조하는 횟수를 알아낼 우아한 해결책을 제공한다. 바로 '레퍼런스 카운팅' 이라는꽤 간단한기법이다. 이 개념은 다음과 같다. 객체가 생성될 때 레퍼런스 카운트는 1 로 설정된다. 객체가 남아 있게 해야 할 때마다, 객체에 retain 메시지를 보내 레퍼런스 카운트를 1 씩 증가시킨다.
[myFraction retain];
배열에 객체를 추가할 때처럼, Foundation 프레임워크에 있는 몇몇 메서드도 이 레퍼런스 카운트를 증가시킨다.
객체가 더이상 필요 없어지면 객체에 release 메시지를 보내 레퍼런스 카운트를 1 씩 감소시킨다.
[myFraction release];
객체의 레퍼런스 카운트가 0 이 되면, 시스템은 객체가 이제 필요 없다고 알게되므로(이론적으로, 더는 참조되지 않으므로) 메모리에서 제거한다('할당 해제' 한다). 이것은 객체에 dealloc 메시지를 보내어 수행한다.
이 전략을 성공시키려면 프로그램이 톨깅}가는 동안 레퍼런스 카운트가 적절히 증가되고 감소되도록 프로그래머가 애를 써야 한다. 시스템이 작업을 일부 처리해 주기는 하나, 전부 해주지는 못한다.
레퍼런스 카운팅에 대해 좀더 상세히 살펴보자. 객체에 retainCount 메시지를 보내 객체의 레퍼런스(혹은 '리테인') 카운트를 얻어을 수 있다. 이 메서드를 쓸 일은 거의 없겠지만, 여기서는 설명해 줄 목적으로, 유용하게 사용되는 예를 골랐다(프로그램 17.1을 보라). 이 메서드는 NSUInteger 형 unsigned 정수를 반환한다.
프로그램 17.1
// 레퍼런스 카운팅 소개
#import <Foundation/NSObject.h>
#import <Foundation/NSautoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSValue.h>
int main (int argc, char *argvp[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSNumber *myInt = [NSNumber numberWithInteger: 100];
NSNumber *myInt2;
NSMutableArray *myArr = [NSMutableArray array];
NSLog (@"myInt retain count = %lx",
(unsigned long) [myInt retainCount]);
[myArr addObject: myInt];
NSLog (@"after adding to array = %lx",
(unsigned long) [myInt retainCount]);
myInt2 = myInt;
NSLog (@"after asssignment to myInt2 = %lx",
(unsigned long) [myInt retainCount]);
[myInt retain];
NSLog (@"myInt after retain = %lx",
(unsigned long) [myInt retainCount]);
NSLog (@"myInt2 after retain = %lx",
(unsigned long) [myInt2 retainCount]);
[myInt release];
NSLog (@"after release = %lx",
(unsinged long) [myInt retainCount]);
[myArr removeObjectAtIndex: 0];
NSLog (@"after removal from array = %lx",
(unsigned long) [myInt retainCount]);
[pool drain];
return 0;
}
프로그램 17.1 의 출력결과
myInt retain count = 1
after adding to array = 2
after asssignment to myInt2 = 2
myInt after retain = 3
myInt2 after retain = 3
after release = 2
after removal from array = 1
NSNumber 객체인 myInt 가 정수 값 100 으로 설정되고, 출력 결과를 보면 myInt 의 최초 리테인 카운트로 1 이 설정되어 있다. 그 다음으로, addObject: 메서드를 사용하여 이 객체를 myArr 배열에 추가한다. 이제 레퍼런스 카운트가 2로 올라간다. addObject: 는 자동으로 레퍼런스 카운트를 을린다. addObject: 의 문서를 보면, 이 사실에 대한 설명을 볼 수 있다. 객체를 컬렉션에 추가하면 레퍼런스 카운트가 증가된다. 이 말은, 추가한 객체를 나중에 릴리스해도 배열 안에서는 참조가 유효하고, 해제되지 않았다는 의미다.
그 다음으로, myInt 를 myInt2 에 대입한다. 그러나 이 작업으로는 레퍼런스 카운트가 증가하지 않기 때문에, 나중에 문제가 발생할 가능성도 있다. 예를 들어, myInt 의 레퍼런스 카운트가 0 으로 줄어들어서 저장된 공간이 릴리스되었다면 myInt2 는 유효하지 않은 객체 참조를 갖게 될 것이다. (myInt를 myInt2에 대입하더라도 실제 객체는 복사되지 않는다는 점을 기억하자. 객체가 있는 메모리를 가리키는 포인터값만 복사된다.)
myInt 가 (myInt2 에 의해) 다른 참조를 갖게 되었으므로 retain 메시지를 보내 레퍼런스 카운트를 증가시키자. 프로그램 17.1 의 다음줄에서 이 작업을한다. retain 메시지를 보내고 나면 레퍼런스 카운트가 3 이 된다. 첫 레퍼런스는 객체 자신이고, 두 번째는 배열에서, 세 번째는 대입에서 생겨났다. 배열에 원소를 저장하면 자동으로 레퍼런스가 올라가지만, 다른 변수에 대입하는 것은 레퍼런스를 증가시키지 않으므로 직접 해줘야 한다. myInt 와 myInt2 를 출력한 결과를 보면 둘 다 레퍼런스 카운트가 3 이다. 모두 메모리상 동일한 객체를 참조하기 때문이다.
프로그램에서 myInt 객체를 다 썼다고 해보자. 객체에 release 메시지를 보내어 시스템에게 사용이 끝났다고 알릴 수 있다. 레퍼런스카운트가 0 이 아니기 때문에 이 객체의 다른참조(배열과 myInt2)는 아직 유효하다. 시스템은 레퍼런스 카운트가 0 이 되지 않는한 메모리를 해제하지 않는다.
만일 removeObjectAtIndex: 메서드를 사용하여 배열 myArr 의 첫째 원소를 제거하면 myInt 의 레퍼런스 카운트는 자동으로 1 로 줄어든다. 일반적으로 컬렉션에서 객체를 제거하면 객체의 레퍼런스 카운트가 줄어드는 부작용이 생긴다. 따라서, 다음 코드는 문제가 발생할 여지가 있다.
myInt = [myArr ObjectAtIndex: 0];
[myArr removeObjectAtIndex: 0]
이 경우 removeObjectAtIndex: 메서드가 호출된 후, 참조하는 객체의 레퍼런스 카운트가 0 으로 줄어들면 myInt 는 유효하지 않게 된다. 여기서는 당연히 배열에서 myInt 를 받은 후 리테인하여, 다른 곳에서 참조해 오는 레퍼런스에 어떤 일이 발생해도 문제가 없도록 만들어 주어 해결한다.
레퍼런스 카운트와 스트링
프로그램 17.2 는 스트링 객체의 레퍼런스 카운트가 어떻게 작동하는지 보여 준다.
프로그램 17.2
// 스트링 객체와 레퍼런스 카운팅
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArray.h>
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool == [[NSAutoreleasePool alloc] init];
NSString *myStr1 = @"Constant string";
NSString *myStr2 = [NSString stringWithString: @"string 2"];
NSMutableString *myStr3 = [NSMutableString stringWithString: @"string 3"];
NSMutableArray *myArr = [NSMutableArray array];
NSLog (@"Retain count: myStr1: %lx, myStr2: %lx, myStr3: %lx",
(unsigned long) [myStr1 retainCount],
(unsigned long) [myStr2 retainCount],
(unsigned long) [myStr3 retainCount]);
[myArr addObject: myStr1];
[myArr addObject: myStr2];
[myArr addObject: myStr3];
NSLog (@"Retain count: myStr1: %lx, myStr2: %lx, myStr3: %lx",
(unsigned long) [myStr1 retainCount],
(unsigned long) [myStr2retainCount],
(unsigned long) [myStr3 retainCount]);
[myArr addObject: myStr1];
[myArr addObject: myStr2];
[myArr addObject: myStr3];
NSLog (@"Retain count: myStr1: %lx, myStr2: %lx, myStr3: %lx",
(unsigned long) [myStr1 retainCount],
(unsigned long) [myStr2retainCount],
(unsigned long) [myStr3 retainCount]);
[myStr1 retain];
[myStr2 retain];
[myStr3 retain];
NSLog (@"Retain count: myStr1: %lx, myStr2: %lx, myStr3: %lx",
(unsigned long) [myStr1 retainCount],
(unsigned long) [myStr2 retainCount],
(unsigned long) [myStr3 retainCount]);
// myStr3의 레퍼런스 카운트를 다시 2로 낮춘다.
[myStr3 release];
[pool drain];
return 0;
}
프로그램 17.2 의 출력결과
Retain count: myStr1: ffffffff, myStr2: ffffffff, myStr3: 1
Retain count: myStr1: ffffffff, myStr2: ffffffff, myStr3: 2
Retain count: myStr1: ffffffff, myStr2: ffffffff, myStr3: 3
NSString 객체인 myStr1 에 NSConstantString @"Constant string" 을 대입한다. 스트링 상수를 넣을 공간은 여타 객체와는 다른 방식으로 할당된다. 스트링 상수는 릴리스 해줄 방법이 없으므로 레퍼런스 카운트 기법을 쓸수없다. 이 때문에 retainCount 메시지를 myStr1 에 보내면 이 0xffffffff 값이 반환된다(이 값은 표준 헤더파일 limits.h 에 정의된 최대 크기의 unsigned integer 값 혹은 UINT_MAX 다).
어떤 시스템에서는 프로그램 17.2 의 스트링 상수에 대한 리테인 카운트반환 값이 0x7fffffff 인데, 이 값은 가능한 최대 크기의 signed integer 값 혹은 INT_MAX 다. |
스트링 상수로 초기화된, 수정 불가능한 스트링도 마찬가지다. myStr2 의 리테인 카운트로 확인할 수 있듯이, 마찬가지로 리테인 카운트를 갖지 않는다.
여기서 시스템은 똑똑하게도 수정 불가능한 스트링 객체가 스트링 상수 객체로 초기화 된다는 것을 알아낸다. Leopard 가 발표되기 전에는 이렇게 최적화되지 않아서 myStr2 역시 리테인 카운트를 가졌을 것이다. |
NSMutableString *myStr3 = [NSMutableString stringWithString: @"string 3"];
위 명령문에서 변수 myStr3 은 스트링 문자상수 @"string 3" 의 사본으로 만든 스트링으로 설정된다. stringWithString: 메시지가 NSMutableString 클래스에 전해지기 때문에, 프로그램이 실행되는 동안에 스트링 내용이 바뀔 수 있다. 그리고 상수 문자 스트링은 값이 변할 수 없으므로, 시스템은 myStr2 에서 했듯이, myStr3 변수가 그냥 상수 스트링 @"string 3" 를 가리키도록 설정할 수 없다.
따라서, 출력 결과에도 나오지만, 스트링 객체 myStr3 는 레퍼런스 카운트를 갖는다. 이 스트링을 배열에 더하거나 retain 메시지를보내어, 레퍼런스 카운트는 바꿀 수 있다. 이는 마지막 두 차례 NSLog 호출에서 확인할 수 있다. Foundation 의 stringWithString: 메서드는 이 객체가 생성되는 순간, 이것을 오토릴리스 풀에 추가한다. Foundation 의 arary 메서드 역시 배열 myArr 를 풀에 추가한다.
오토릴리스 풀 자체가 릴리스되기 전에 myStr3 부터 릴리스된다. 그 결과, 레퍼런스 카운트가 2 로 떨어진다. 그 후, 오토릴리스 풀이 릴리스되면서 이 객체의 레퍼런스 카운트는 0 이 되고 결국 할당이 해제된다. 어떻게 해서 이렇게 되는것일까? 오토릴리스 풀이 릴리스되면, 풀안에 있는 모든 객체가 autorelease 메시지를 받은 횟수만큼 release 메시지를 받게 된다. stringWithString: 메서드가 스트링 객체 myStr3 를 만들 때, 오토릴리스 풀에도 추가했으므로 이 객체 역시 release 메시지를 받는다. 그 결과로 레퍼런스 카운트가 1 로 떨어진다. 오토릴리스 풀 안의 배열이 릴리스되면 배열에 들어있는 각 원소도 릴리스된다. 따라서 풀에서 myArr 가 릴리스되면 myStr3 가 담은 원소들도 release 메시지를 받는다. 그 결과로 레퍼런스 카운트는 0 이 되고, 객체가 해제된다.
그러나 객체를 과하게 릴리스하지 않도록 주의해야 한다. 프로그램 17.2 에서 풀이 릴리스되기 전에 myStr3 의 레퍼런스 카운트를 2 보다작게 만들어 놨다면, 풀은 유효하지 않은 객체의 레퍼런스를 담게 될 것이다. 그 후 풀이 릴리스되면 이 유효하지 않은 객체의 참조 때문에 일반 프로그램은 세그먼트 결함(segmentation fault)오류를 내며 비정상 종료될 것이다.
레퍼런스 카운트와 인스턴스 변수
인스턴스 변수를 다룰 때도 레퍼런스 카운트에 주의를 기울여야 한다. 예를들어, AddressCard 클래스의 setName: 메서드를 다시 살펴보자.
-(void) setName: (NSString *) theName
{
[name release];
name = [[NSString alloc] initWithString: theName];
}
setName:을, name 객체를 소유하지 않는 방식으로 정의했다고 가정해 보자.
-(void) setName: (NSString *) theName
{
name = theName;
}
이 버전은 사람 이름을 나타내는 스트링을 받아 name 인스턴스 변수에 저장한다. 매우 분명해 보이지만, 다음과 같이 메서드를 호출한다면 어떻게 될까?
NSString *newName;
...
[myCard setName: newName];
newName 이 주소록에 추가하고 싶은 사람 이름을 임시로 저장하는 공간이고, 이를 후에 릴리스한다고 해보자. myCard 의 name 인스턴스 변수에 무슨 일이 발생할까? 이미 파괴된 객체를 참조하기 때문에 name 필드는 더는 유효하지 않게된다. 이런 이유로 여러분의 클래스에서 자신의 멤버 객체를 소유해야 하는 것이다. 멤버 객체를 소유하면 이 객체들이 우연하게라도 해제되거나 수정될까 봐 염려하지 않아도 된다.
다음 몇 가지 예제를 보면 이 점이 더 상세히 보인다. 먼저, 스트링 객체 str 을 인스턴스 변수로 갖는 새 클래스 ClassA 를 정의하자. 이 변수를 위한 세터와 게터 메서드만 작성한다. 여기서는 메서드를 자동 생성하지 않고 직접 작성해 진행 상황을 정확히 살펴볼 것이다.
프로그램 17.3
// 레퍼런스 카운팅 소개
#import <Foundation/NSObject.h>
#import <Foundation/NSautoreleasePool.h>
#import <Foundation/NSString.h>
@interfacd ClassA: NSObject
{
NSString *str;
}
-(void) setStr: (NSString *) s;
-(NSString *) str;
@end
@implementation ClassA
-(void) setStr: (NSString *) s
{
str = s;
}
-(NSString *) str
{
return str;
}
@end
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
ClassA *myA = [[ClassA alloc] init];
NSMutableString *myStr = [NSMutableString stringWithString: @"A string"];
NSLog (@"myStr retain count: %x", [myStr retainCount]);
[myA setStr: myStr];
NSLog (@"myStr retain count: %x", [myStr retainCount]);
[myA release];
[pool drain];
return 0;
}
프로그램 17.3 의 출력결과
myStr retain count: 1
myStr retain count: 1
프로그램은 ClassA 의 객체 myA 를 생성하고 세터 메서드를 이용해 myStr 로 지정한 스트링 객체를 자신의 인스턴스 변수에 설정한다. myStr의 레퍼런스 카운트는 setStr: 메서드를 호출하기 전에든 후에든 모두 1 이다. 예상했겠지만, 이 메서드는 그저 인수로 넘어온 값을 자신의 인스턴스변수 str 에 저장하기만 한다. 그러나 만일 프로그램이 setStr: 메서드를 호출한 다음 myStr 을 릴리스하면, 인스턴스 변수 str 안에 저장된 값은 이제 유효하지 않게 된다. 참조하는 객체의 레퍼런스 카운트는 0 으로 줄어들고 차지한 메모리 공간이 해제되기 때문이다.
프로그램 17.3에서 오토릴리스 풀이 릴리스될 때 이 현상이 발생한다. myStr 객체를 직접 풀에 추가해 주지는 않았지만, 이 스트링 객체는 stringWithString: 메서드로 생성되었고 이 메서드가 오토릴리스 풀에 객체를 추가한다. 그러므로 풀이 릴리스될 때, myStr 도 릴리스된다. 따라서 풀이 릴리스된 다음에 이 객체에 접근하려 한다면, 유효하지 않을 것이다.
프로그램 17.4 는 setStr: 메서드를 수정해 str 의 값을 리테인 하도록 만든다. 이를통해 누군가 나중에 str 이 참조하는 객체를 릴리스하지 못하게 한다.
프로그램 17.4
// 객체 리테인하기
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArray.h>
@interface ClassA: NSObject
{
NSString *str;
}
-(void) setStr: (NSString *) s;
-(NSString *) str;
@end
@implementation ClassA
-(void) setStr: (NSString *) s
{
str = s;
[str retain];
}
-(NSString *) str
{
return str;
}
@end
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSString *myStr = [NSMutableString stringWithString: @"A string"];
ClassA *myA = [[ClassA alloc] init];
NSLog (@"myStr retain count: %x", [myStr retainCount]);
[myA setStr: myStr];
NSLog (@"myStr retain count: %x", [myStr retainCount]);
[myStr release];
NSLog (@"myStr retain count: %x", [myStr retainCount]);
[myA release];
[pool drain];
return 0;
}
프로그램 17.4 의 출력결과
myStr retain count: 1
myStr retain count: 2
myStr retain count: 1
myStr 의 레퍼런스 카운트는 setStr: 메서드를 호출하고 난 뒤 2 로 증가하였다. 즉, 앞에서 드러났던 문제가 해결되었다. 그 다음에 myStr 를 릴리스해도 레퍼런스 카운트가 1 이므로 인스턴스 변수를 통한 참조는 여전히 유효하다.
alloc을 사용하여 myA 를 생성했기 때문에, 이것은 직접 릴리스해 줘야 한다. 직접 릴리스하는 대신 autorelease 메시지를 보내 오토릴리스 풀에 추가해도 된다.
[myA autorelease];
원한다면 객체를 생성하자 마자 바로 오토릴리스 풀에 추가해도 된다. 객체를 오토릴리스 풀에 추가한다고 해서 객체가 릴리스되거나 유효하지 않는 것은 아니다. 그저, 나중에 릴리스될 거라고 표시하는 것뿐이다. 풀이 릴리스되고, 객체의 레퍼런스 카운트가 0 이 되어 해제되기 전까지는 객체를 계속 사용할 수 있다.
그러나 여전히 몇 가지 문제가 남아 있다. setStr: 메서드는 스트링 객체를 인수로받아 리테인하는 작업을 수행한다. 그런데 그 객체는 대체 언제 릴리스 되는걸까? 그리고 덮어쓸 인스턴스 변수 str 의 예전 값은 어떻게 될까? 이 값을 메모리에서 해제해 줘야하지는않을까? 프로그램 17.5 에 이 문제를 해결할 방책이 있다.
프로그램 17.5
// 레퍼런스 카운팅 소개
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArray.h>
@interface ClassA: NSObject
{
NSString *str;
}
-(void) setStr: (NSString *) s;
-(NSString *) str;
-(void) dealloc;
@end
@implementation ClassA
-(void) setStr: (NSString *) s
{
// 사용을 마친 오래된 객체를 해제한다.
[str autorelease];
// 다른 누군가가 릴리스할 것을 대비해 인수를 리테인한다.
str = [s retain];
}
-(NSString *) str
{
return str;
}
-(void) dealloc {
NSLog (@"ClassA dealloc");
[str release];
[super dealloc];
}
@end
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSString *myStr = [NSMutableString stringWithString: @"A string"];
ClassA *myA = [[ClassA alloc] init];
NSLog (@"myStr retain count: %x", [myStr retainCount]);
[myA autorelease];
[myA setStr: mystr];
NSLog (@"myStr retain count: %x", [myStr retainCount]);
[pool drain];
return 0;
}
프로그램 17.5 의 출력결과
myStr retain count: 1
myStr retain count: 2
ClassA dealloc
setStr: 메서드는 먼저 현재 str 인스턴스 변수에 저장된 것을 오토릴리스한다. 이는, 나중에 릴리스 되도록 하겠다는 의미다. 만일 프로그램이 실행되는 동안 메서드가 여러 번 호출되어 동일한 필드에 다른 값을 설정하는 상황이라면 이것은 매우 중요하다. 새 값이 저장될 때마다, 이전 값은 릴리스되도록 표시해야 한다. 이전값이 릴리스된 뒤에 새 값이 리테인되고 str 필드에 저장된다. 다음 메시지 표현식은 retain 메서드가 수신자를 반환한다는 점을 활용한다.
str = [s retain];
만일 str 변수가 nil 이어도 아무 문제 없다. Objective-C 런타임은 모든 인스턴스 변수를 마로 초기화하고, nil 에 메시지를 보내도 괜찮다. |
dealloc 메서드는 새로울 것이 없다. 15장 「숫자, 스트링, 컬렉션」에 나왔던 AddressBook, AddresCard 클래스에서 이미 만난 적 있다. dealloc 을 재정의하여 메모리를 릴리스해야 할 때(즉, 레퍼런스 카운트가 0이 될 때), str 인스턴스 변수가 참조하는 마지막 객체를 깔끔하게 처분할 수 있다. 이 경우, 시스템은 NSObject 에서 상속받은 dealloc 을 호출한다. 보통은 dealloc 을 재정의 하지 않는다. 그러나 여러분의 메서드 안에서 리테인하거나 alloc 으로 할당하거나, (다음 장에서 설명할 복사 메서드중 하나로) 복사했다면, dealloc 을 재정의하여 이것들을 정리해야할 수도 있다. 다음 명령문은 먼저 str 인스턴스 변수를 릴리스하고, 부모의 dealloc 메서드를 호출하여 작업을 마친다.
[str release];
[super dealloc];
dealloc 메서드 내에 NSLog 호출을 두어 메서드가 호출되었을 때 메시지가 표시되게 하였다. 이 메시지는 오토릴리스 풀이 릴리스될 때 ClassA 객체가 적절히 해제되었는지 확인하기 위해 표시한다.
혹시 세터 메서드 setStr: 에 숨겨진 마지막 함정을 발견했는가. 프로그램 17.5 를 다시 한 번 보자. myStr 이 수정 불가능한 스트링이 아니라 수정 가능한 스트링이었고, setStr: 메서드가 호출된 뒤에 myStr 의 문자를 한두 개 바꾸었다고 가정해 보자. myStr 가 참조하는 스트링에 변회를 가하면, 인스턴스 변수가 참조하는 스트링에게도 동일한 영향을 끼치게 된다. 사실 둘이 동일한 객체를 참조하기 때문이다. 마지막 문장을 다시 읽고 요점을 분명히 이해하자. 또한 myStr 를 완전히 새로운 스트링 객체로 만들면 이 문제가 발생하지 않는다. 이 문제는, 스트링에서 문자를 어떤 식으로든 하나 이상 수정하는 경우에만 발생한다.
세터의 인수로부터 스트링을 보호하고 완전히 독립되도록 만들고 싶다면, 세터내에서 스트링의 새로운 사본을 만들어서 문제를 해결할 수 있다. 이런 이유로 15장 에서 AddressCard 의 메서드인 setName: 과 setEmail: 에서 name, email 멤버의 사본을 만든다.
오토릴리스 예제
이 장의 마지막 예제를 보고 레퍼런스카운트와 리테인, 객체의 릴리스/오토릴리스가 어떻게 동작하는지 완벽히 이해하자. 프로그램 17.6 은 인스턴스 변수 하나와 상속받은 메서드만 있는 연습용 클래스 Foo 를 정의한다.
프로그램 17.6
#import <Foundation/NSObject.h>
#import <Foundation/NSAutoreleasePool.h>
@interface Foo: NSObject
{
int x;
}
@end
@implementation Foo
@end
int main (int argc, char *argv[])
{
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
Foo *myFoo = [[Foo alloc] init];
NSLog (@"myFoo retain count = %x", [myFoo retainCount]);
[pool drain];
NSLog (@"after pool drain = %x", [myFoo retainCount]);
pool = [[NSAutoreleasePool alloc] init];
[myFoo autorelease];
NSLog (@"after autorelease = %x", [myFoo retainCount]);
[myFoo retain];
NSLog (@"after retain = %x", [myFoo retainCount]);
[pool drain];
NSLog (@"after second pool drain = %x", [myFoo retainCount]);
[myFoo release];
return 0;
}
프로그램 17.6 의 출력결과
myFoo retain count = 1
after pool drain = 1
after autorelease = 1
after retain = 2
after second pool drain = 1
프로그램은 새로운 Foo 객체를 생성해 변수 myFoo 에 대입한다. 보다시피 초기 리테인 키운트는 1 이다. 이 객체는 아직까지는 오토릴리스 풀에 속하지 않았으므로 풀을 릴리스해도 객체가 쓸모없어지지는 않는다. 그 다음 새로운 풀이 생성되고 myFoo에 autorelease 메시지를 보내 풀에 추가한다. 객체를 오토릴리스 풀에 추가해도 레퍼런스 카운트에는 별다른 영향이 없다. 그저 객체가 나중에 릴리스 된다고 표시할 뿐이므로, 여기서 레퍼런스 카운트는 그대로임에 다시 한번 주의를 기울이자.
이제 myFoo 에 retain 메시지를 보낸다. 그 결과 레퍼런스 카운트는 2 가 된다. 풀을 두 번째로 릴리스하면 앞서 autorelease 메시지를 받았던 myFoo 는 풀이 릴리스될 때 release 메시지를 받게 되므로 레퍼런스 카운트가 1 로 줄어든다.
풀이 릴리스되기 전에 myFoo 가 리테인 되었으므로 리테인 카운트는 1 씩 줄어들어도 0 보다는 크다. 따라서 풀이 풀린 다음에도 myFoo 는 계속 살아 있고 여전히 유효한 객체다. 물론, 이제는 그것을 직접 릴리스해 줘야 한다. 프로그램 17.6 에서는 적절히 처리하여 메모리 누수를 피했다.
아직 이 내용이 알듯말듯 하다면 오토릴리스 풀에 대한 설명을 다시 한 번 읽어보자. 프로그램 17.6 을 이해하고 나면, 오토릴리스 풀과 그 동작을 완전히 이해할 것이다.
메모리 관리 규칙 요약
이 장에서 메모리 관리 에 대해 배운 것을 요약해 보자.
- 객체를 릴리스하면 메모리를 해제하게 된다. 이 경우 프로그램이 실행되는 동안 객체를 많이 생성했다면, 문제가 생길 수 있다. 이때 유용한 규칙은 생성하거나 리테인한 객체를 다 쓰고나면 릴리스해 주는 것이다.
- 객체에 release 메시지를 보낸다고 객체가 반드시 파괴되지는 않는다. 객체의 레퍼런스 카운트가 0 이 될 때, 객체는 파괴된다. 시스템은 객체에 dealloc 메시지를 보내 메모리를 해제한다.
- 오토릴리스 풀은 자신이 릴리스될 때 자동으로 객체들을 릴리스해 준다. 시스템은 풀에 있는 각 객체가 오토릴리스될 때마다 객체에 release 메시지를 보낸다. 오토릴리스 풀에 든 객체 중 레퍼런스 카운트가 0 인 객체는 dealloc 메시지를 받고 파괴된다.
- 메서드 내에서 객체가 더는 필요없지만 반환해 줘야 한다면, autorelease 메시지를 보내 나중에 릴리스하도록 표시한다. autorelease 메시지는 객체의 레퍼런스 카운트에는 영향을 끼치지 않는다. 따라서, 이를 이용해 메시지 송신자도 객체를 사용하면서 오토릴리스 풀이 릴리스될 때, 객체가 해제되도록 만들수있다.
- 응용 프로그램이 종료될 때, 여러분의 객체가 오토릴리스 풀에 있었든 없었든 차지하던 메모리는 모두 해제된다 .
- (코코아 응용 프로그램처럼)더 복잡한 응용 프로그램을 개발하는 경우, 프로그램이 실행되는 동안 오토릴리스 풀을 생성하거나 파괴할 수 있다(코코아 응용 프로그램의 경우, 이벤트마다 이 일이 발생한다). 이 경우, 오토릴리스 풀 자체가 릴리스될 때 자동 해제에서 객체가 살아남길 원한다면 명시적으로 리테인해 주어야 한다. 레퍼런스 카운트가 오토릴리스 메시지를 받은 횟수보다 많은 객체들은 모두 풀이 릴리스되어도 살아남는다 .
- alloc, copy 메서드(혹은 allocWithZone:, copyWithZone:, mutableCopy: 메서드)로 객체를 직접 생성하면, 우리가 직접 릴리스해 줘야 한다. 다시 말해 객체를 retain 할 때마다 그 객체를 release 하거나 autorelease 해 주어야 한다.
- 앞의 규칙에서 설명한 메서드를 제외한 다른 메서드가 반환한 객체에 대해서는 릴리스 걱정을 하지 않아도 된다. 이 객체들을 릴리스하는 일은 여러분의 몫이 아니다. 메서드가 이 객체들을 오토릴리스해야 한다. 이는, 여러분의 프로그램에서 처음으로 오토릴리스 풀을 만드는 이유다. stringWithString: 같은 메서드는 새로 생성된 스트링 객체에 자동으로 autorelease 메시지를 보내 풀에 추가한다. 풀을 설정하지 않았다면, 풀 없이 객체를 오토릴리스하려 했다는 메시지를 받을 것이다.
가비지 컬렉션
여기까지는 프로그램이 '메모리-관리' 런타임 환경에서 돌아가도록 작성했다. 앞에 요약된 메모리 관리 규칙은 오토릴리스 풀, 객체 리테인/릴리스, 객체 소유권같은 이슈를 다루는 환경에 적용된다.
Objective-C 2.0 부터는 메모리 관리를 다른 형태로 할 수 있다. 바로 '가비지 컬렉션' 기법으로 말이다. 가비지 컬렉션을 사용하면 객체의 리테인과 릴리스, 오토릴리스 풀, 리테인 카운트에 대해 걱정할 필요가 없다. 시스템이 지동으로 어떤 객체가 어떤 객체를 소유하는지에 대한 정보를 끊임없이 알아내고, 프로그램이 실행되는 동안 메모리 공간이 필요하면 더는 참조되지 않는 객체들을 자동으로 메모리에서 해제한다(혹은 가비지 컬렉팅한다).
하지만 모든게 이처럼 간단하기만 하다면, 처음부터 가비지 컬렉션을 활용하고 메모리 관리에 대한 무수한 내용을 전부 건너뛰었을 것이다. 그렇지 않은 데에는 세가지 이유가 있다. 먼저, 가비지 컬렉션을 지원하는 환경에서도 누가 여러분의 객체를 소유하는지 알고, 객체가 더는 없어도 되는 시점이 언제인지 아는 것이 최선이다. 이를 통해 프로그램에서 객체의 상호 관계와 생명주기를 이해하게 되어 코드를 더 꼼꼼하게 작성하게 되기 때문이다.
두번째는, 앞서 언급했듯이 아이폰 런타임 환경에서는 가비지 컬렉션이 지원되지 않기 때문이다. 따라서 여러분이 아이폰용 프로그램을 개발한다면 선택의 여지가 없다.
세 번째 이유는 라이브러리 루틴, 플러그인, 공유 코드를 게괄하는 상황에만 적용된다. 이런 코드는 가비지 컬렉션 프로세스와 비(非)가비지 컬렉션 프로세스 양쪽에 로드될 수 있다. 그러니 양쪽의 환경에서 모두 작동하도록 작성해야만 한다. 즉, 지금까지 설명한 메모리 관리 기법을 사용하여 코드를 작성해야 한다는 얘기다. 또한 여러분의 코드를 가비지 컬렉션을 활성화한 상태와 활성화 하지 않은 상태에서 모두 테스트 해봐야 한다는 의미다.
가비지 컬렉션을 사용하기로 마음을 먹었다면, Xcode 로 프로그램을 빌드할 때 가비지 컬렉션을 켜줘야 한다. Project > Edit Project Settings 메뉴에서 할 수 있다. 'GCC 4.0-Code Generation' 설정에서 Objective-C Garbage Collection 이라는 항목을 찾을 수 있다. 기본 값인 Unsupported 를 Required 로 바꿔 주면 자동 가비지 컬렉션을 활성화한 상태로 프로그램을 빌드한다(그림 17.1 을 보라).
가비지 컬렉션이 활성화되어도 프로그램은 여전히 retain, autorelease, release, dealloc 메서드를 호출할 수 있다. 그러나 이 메서드들은 모두 무시된다. 이런 식으로 프로그램이 메모리 관리 환경과 가비지 컬렉션 환경에서 모두 잘 동작하도록 개발할 수 있다. 그러나 만일 여러분의 코드가 양쪽 환경에서 동작해야 한다면, 여러분이 제공하는 dealloc 메서드에서는 아무 작업도 하지 못한다. 이미 언급한 대로,가비지 컬렉션 환경에서는 dealloc 호출이 무시되기 때문이다.
이 장에서 설명한 메모리 관리 기법이면 대부분의 응용 프로그램을 개발하기에 충분할 것이다. 그러나 멀티스레드 응용프로그램과 같이 좀더 고급 프로그래밍으로 들어서면 해줘야 할 일이 더 있다. 이 이슈들과 가비지 컬렉션에 관련된 다른 이슈들에 대해 더 알아보려면 부록 D 「참고자료」를보라. |
연습문제
1. 딕셔너리에 객체를 추가하거나 제거할 때, 객체의 레퍼런스 카운트에 어떤 영향을 끼치는지 확인할 프로그램을 작성하라.
2. NSArray 의 replaceObjectAtIndex:withObject: 메서드는 배열에서 교체되는 객체의 레퍼런스 카운트에 어떤 영향을 줄까? 또 배열에 새로 들어가는 객체에는 어떤 영향이 끼칠까? 이를 확인하는 프로그램을 작성하라. 그 후에 메서드에 대한 문서를 참고하여 결과를 확인하라.
3. 1 부 「Objective-C 2.0」 에서 다룬 Fraction 클래스로 다시 돌아가자. 이 클래스를 수정하여 Foundation 프레임워크에서 사용될 수 있도록 만들라. 그 다음 다양한 MathOps 카테고리 메서드에 적절한 메시지를 추가하여 각 연산 결과로 반환되는 분수를 오토릴리스 풀에 추가하자. 이 작업을 마치면 다음과 같은 명령문을 작성해도 메모리 누수가 발생하지 않는지 확인하자.
[[fractionA add: fractionB] print];
발생하지 않는다면 그 이유가 뭔지 설명하라.
4. 15장의 AddressBook, AddressCard 예제로 돌아가서 dealloc 메서드가 호출될 때마다 메시지를 출력하도록 수정하라. 그 다음, 이 클래스들을 사용하는 예제 프로그램을 실행해서 main 의 끝에 도달하기 전에 프로그램이 쓰는 모든 AddressBook, AddressCard 객체에 메시지가 보내지는지 확인하라.
5. 이 책에서 아무 프로그램이나 두 개 골라서 가비지 컬렉션을 켜고 Xcode 로 빌드하고 실행해 보라. 가비지 컬렉션이 켜져 있을 때, retain, autorelease, relaese 같은 메서드 호출이 무시되는지를 확인하라.
Notes
- ↑ (옮긴이) '풀{pool)'이라는 용어 덕택에, 풀에 들어 었는 객체들을 릴리스할 때, '물을 뺀다(drain)'는 표현을 사용한다