객체의 생성과 소멸

OSXDEV

Jump to: navigation, 찾기

목차

[편집] 객체의 생성과 소멸

객체지향개발환경에서 객체의 생성과 소멸은 가장 기본적이면서도 가장 중요한 부분이라 할 수 있다. 여기서는 Cocoa개발환경 특히 Objective-C객체의 생성과 소멸에 대해서 알아보자.
C++나 Java와 달리 Objective-C에서는 따로 객체를 생성하거나 소멸하는데 사용하는 키워드가 존재하지 않는다. 또한 Objective-C는 안타깝게도 garbage collecting기능을 갖추고 있지도 않다. 하지만, 여러분들이 Objective-C에서의 룰을 잘 이해한다면 그리 걱정할 바가 아니다. Objective-C에는 여러모로 펀리하게 생성소멸관리를 도와주는 장치들이 준비되어 있기때문이다.

[편집] 1. NSObject의 인스턴스를 만들어보자.


NSObject의 인스턴스를 만드는 방법은 몇가지가 있다. 하지만, 가장 쉬운 것부터 해보자. 다음의 코드는 anObject변수에 NSObject의 인스턴스를 생성해서 대입시킨다.

anObject = [NSObject new];


NSObject는 Cocoa Framework의 루트클래스이고 사실상 아무것도 아니다. 하지만, 루트클래스이기때문에 다른 모든 서브클래스들에 대해서도 동일하게 사용되며 객체의 생성과 소멸을 관리하는 대부분이 NSObject에 구현되어 있다.

위의 코드는 매우 간단하지만, 많은 것을 내포한다. 객체가 메모리상에 할당되며 인스턴스 변수나 객체에 대한 초기화를 하게된다. 이 코드는 NEXTSTEP 어플리케이션 소스에서는 많이 찾아볼 수 있지만, 솔직히 필자는 OpenStep이후 잘 보지못했다. 하지만 지금도 잘 동작한다. 단지 좀 명확하지 못하다는 것일뿐이다. 이 코드는 사실 다음 코드와 똑같다.

anObject = [[NSObject alloc] init];


+new메소드는 클래스메소드로서 클래스에게 +alloc과 -init을 연속적으로 부른다. +alloc메소드는 클래스메소드로 클래스가 instanciate할 때 객체가 필요로 하는 메모리를 할당하는 역할을 한다. -init은 인스턴스메소드로 instanciate된 객체를 최종적으로 초기화하게 된다. 어떻게 보면 C++나 Java의 Constructor메소드는 Objective-C의 이 -init메소드와 같은 놈이라 할 수 있다.
가끔 객체를 초기화 시킬때 필요로 하는 인자를 요구할 수 있는데, 그런 경우는 -init...메스드들을 만들어 나감으로써 가능하다. 예를 들어, 문자열객체를 인자로 넘겨주어야 할경우는 다음과 같은 -init...메소드를 만들면 되겠다.

- initWithString:(NSString *)string;


이 객체의 클래스가 MyObject라 할때 객체의 생성은 다음과 같이 하면 된다.


anObject = [[MyObject alloc] initWithString:@"Sample"];


위의 코드는 -init메소드를 -initWithString:으로 대체시킨 것이다.
물론 한번에 객체를 생성시켜주는 +new도 얼마든지 대체시킬 수 있다. +newWithString:을 만들어 +alloc과 -initWithString:을 불러주면 된다.

이외에도 객체를 바로 만들어주는 클래스메소드들이 존재한다. 예를 들어 NSString클래스를 살펴보면 +string이라는 클래스메소드가 있다. 이 클래스는 빈 문자열객체를 생성시켜준다. 하지만 +new로도 똑같은 일을 할 수 있다. 하지만 경고컨데, +string은 다른 점이 있다.

[편집] 2. retainCount를 이해하자.


혹시나 생성을 했으면, 어떻게 소멸시키는지를 왜 언급하지 않은지 궁금해하는 사람이 있을까? 그래서 한가지를 알려주겠다. NEXTSTEP에서는 -free라는 메소드가 있었다. 이놈은 객체를 소멸시켜주는 것이다. 하지만, OpenStep부터 없어졌다. 그럼 어떻게 하느냐고? 그 답은 소멸시키지 마라이다! 흠, 좋다. 정확히, 소멸시킬 필요가 없다이다. 그럼, 자동으로 소멸시켜준다고? 그건 아니다.
Cocoa Framework에서는 모든 객체가 retainCount를 가지고 있다. retainCount란 쉽게 객체의 reference count라고 할 수 있다. 즉, 얼마나 많이 참조되고 있느냐라는 것이다. garbage collection이 지원되는 언어들과의 차이점은 이 count를 프로그래머가 직접 제어해야 한다는 것이다. NSObject에 구현된 -retainCount는 unsigned int형의 숫자를 돌려주는데, 이 숫자가 얼마나 reference되고 있는지를 알려준다. 프로그래머는 객체에 -retain과 -release를 불러서 retainCount를 증가시키고 감소시킬 수 있다.
객체는 retainCount가 0이되는 순간 소멸된다. 즉, 좀더 자세히 설명하자면, -retain은 객체의 retainCount를 1증가시키고 -release는 1감소시킨 후 0인지 확인하고 0이라면 객체를 완전히 소멸시키게 된다. 사실, 이 retainCount만 정확히 이해한 후 프로그래밍을 할 때 조금만 주의를 기울이고 룰을 지켜준다면, garbage collecting지원언어가 부럽지 않을 만큼 쉽다.
그럼, 이제 처음에 배웠던 간단한 객체생성 코드에서 이 retainCount가 어떻게 동작하는지 알아보자. 쉽게 다음의 코드를 실행시켜보면 간단한 해답이 나온다.


anObject1 = [NSObject new];

anObject2 = [[NSObject alloc] init];

NSLog(@"%d", [anObject1 retainCount]);

NSLog(@"%d", [anObject2 retainCount]);


처음에도 언급했지만, 사실 위 두 생성코드는 같다. 그러니 결과도 같은 결과가 나온다. 답은 1이다. 다음을 또 보자.

anObject = [[NSObject alloc] init];

NSLog(@"%d", [anObject retainCount]);

[anObject retain];
NSLog(@"%d", [anObject retainCount]);

[anObject release];
NSLog(@"%d", [anObject retainCount]);

[anObject release];


이번에는 객체를 생성시킨 다음, -retain과 -release를 불러보았다. retainCount는 어떻게 변할까? 답은 1, 2, 1이다. 하지만, 이 코드에서 중요한 것이 하나 있다. 마지막에 부른 -release는 바로 전에 부른 -release와 달리 객체를 완전히 소멸시켜버릴 것이다. 왜냐하면, retainCount가 0이 되는 순간이니까. 혹시 이 후에 anObject를 사용하면 곧바로 런타임에러가 발생하게 된다. 이미 소멸되어 없어진 객체를 사용할려고 했으니까.
마지막으로 한가지 중요한 것은 처음에 객체를 생성하고 난 뒤 retainCount가 1이라는 것이다. 뭐 당연한거 아니냐라고 생각할 수도 있지만, 필자가 말하고 싶은 것은 +alloc메소드에 있다. +alloc은 객체가 필요한 메모리를 할당하는 것 외에도 retainCount를 0에서 1로 1증가시켜준다는 것을 잊지말자.

[편집] 3. 생성된 객체 사용하기.


retainCount를 이해했다면 이제 본격적으로 응용할 때다. C프로그래밍을 공부할 때, 선생님이나 선배들로부터 귀에 못이 박히도록 들은 잔소리가 있을 것이다.
"메모리가 필요하면 반드시 malloc으로 할당받고 사용이 끝나면 반드시 free해주어라."
돼지같은 프로그램이 되지 않을려면 자신이 할당받은 자원은 반드시 해제해 주어야 한다. 그렇지 않으면, 프로그램이 종료될 때 까지 시스템의 자원들이 폐허가 되어버린다.
객체도 똑같다. 객체를 필요로 해서 받아왔으면 본격적인 사용에 들어가기전에 -retain을 불러주어 객체가 사용되고 있음을 알려야하고, 사용이 끝나면 -release를 불러주어 더이상 필요없는 객체는 소멸되도록 해주어야한다. 단 한가지. C프로그래밍을 가르쳐준 선배들처럼, 필자도 꼭 한마디의 잔소리를 하려한다.
"retain한 객체는 반드시 release해 주어라."
retain만 해주고 release를 하지 않으면 그 객체는 프로그램이 종료될때까지 소멸되지 않는다. 만약, 그 프로그램이 서버프로그램이라면? 아주 치명적인 결과를 얻을 것이다.
만약, 필요해서 retain을 해주고, 필요없어서 release를 잘 해주었는데도 불구하고 프로그램이 런타임에러를 내며 종료된다면, 그것은 프래그래머가 잘못한 것이지 이 룰이 잘못이 아니라는 것을 확실하게 말하고 싶다. 완벽한 프로그램을 만들도록 노력해보라.
그리고 혹시나 해서 덧붙이는데, 언급했듯이 +alloc도 retainCount를 증가시키기 때문에 +alloc도 곧 retain을 한거나 마찬가지로 볼 수 있다. 그러므로, 다음이 되겠지.
"+alloc으로 생성시켰으면 역시나 반드시 release해 주어라."
이제 간단한 예를 하나 들어보겠다. 만약 MyObject클래스를 구현하는데, 이 객체가 -setObject:라는 메소드를 통해 다른 객체를 받아서 사용하고 있다고 하자. -setObject:를 간단히 구현한 코드를 보자.


- (void)setObject:(id)anObject {

[object release];
object = [anObject retain];

}


anObject는 MyObject에 넘겨주는 객체이고 object는 MyObject가 anObject를 받아서 사용하게 될 인스턴스변수다. 이 코드는 anObject를 새로 받을 때 기존의 object에 -release를 불러서 사용이 끝났슴을 알리고, 새로받게 되는 anObject에 -retain을 불러 이제 사용할 것이라 알린다. 참고로 -retain메소드는 리시버(receiver)를 리턴한다.
이렇게 하면, 매번 다른 객체로 -setObject:를 하더라도 정확히 MyObject에 의해 retain/release되어 객체가 사용중 소멸되지 않을 뿐만 아니라, 아무도 사용하지 않게 되는 객체는 소멸됨으로써 깨끗한 프로그램이 될 수 있다.

[편집] 4. 객체가 소멸될 때.


앞에서 생성한 객체를 소멸시켜 보았다. 이제 필요없는 시스템자원을 돌려주게 되어 기쁜가? 하지만, 한가지 남은 것이 있다. 바로 위의 예제의 경우 MyObject가 소멸될 때, 가지고 있던 인스턴스변수에 남아있는 object는 어떻게 할 것인가? 결국 이 object는 사용이 끝났는데, -release가 불려지지 않는다면, 소멸되지 않을 것이다. 객체가 소멸될 때, 그 객체의 인스턴스변수에 존재하는 객체들마저 자동으로 처리될 거라고는 절대로 기대하지 마라.
여기에서 -dealloc메소드를 소개한다. 이 메소드는 Java에서 finalize()와 비슷한 놈이라 할 수 있다. 객체의 retainCount가 0이되어 소멸하게 되면, Objective-C런타임은 객체에 -dealloc메소드를 불러 소멸시키게 된다. 하지만, 결코, 프로그래머가 -dealloc을 부르지는 않는다. 소멸이 필요하면 자동으로 불려진다. 당신이 해야할 일은 -dealloc메소드만 적절하게 구현해 놓으면 끝이다.
-dealloc메소드를 구현할 때 반드시 지켜야하는 것은 단 한가지이다. 마지막에 super의 -dealloc을 불러주기만 하면 된다. 바로 앞의 예제의 MyObject의 경우 -dealloc메소드의 구현은 다음과 같게 된다.

- (void)dealloc {

[object release];
[super dealloc];

}


MyObject가 사용하기 위해 retain을 걸어 놓았던, object를 마지막으로 release해준다. 그리고 나머지는 superclass에 맞긴다. 만약 super에 -dealloc을 부르지 않으면, 객체가 소멸된 것처럼 보이지만, 완전히 소멸되지 않는다는 것은 이제 굳이 설명하지 않아도 알것이다.

[편집] 5. autorelease ?


만약 당신이 객체를 하나 생성하고 리턴해주는 메소드를 만든다고 생각해보자. 필자가 앞에서 무척이나 강조했던 "retain한 객체는 반드시 release해주라"라는 룰을 지킬 수 있을까? 혹시나 아예 이 생각조차 들지 않았다면, 더 이상의 진도는 무의미하니 앞의 내용들을 완벽히 이해하려고 노력해보라. 필자가 이런 말을 한다고 너무 상심하지는 말고 다음의 코드를 한번 보자.


- (id)objectForMe {

id anObject = [[NSObject alloc] init];
...
[anObject release];
return anObject;

}


단도직입적으로 위의 코드는 완전히 틀렸다. 이해가 간다면 왜 autorelease가 필요한지 알것이다. 자세히 설명하면, 위의 코드에서 리턴해주는 anObject는 -release메소드가 불리면서 소멸해버려 전혀 쓸모없을 뿐만 아니라, 런타임에러도 불사할것이다. 쉽게 위의 코드를 다시 써보자.


- (id)objectForMe {

id anObject = [[NSObject alloc] init];
...
return [anObject autorelease];

}


이번에는 release시키지 않고 -autorelease의 리턴값을 돌려주었다. -autorelease는 리시버를 리턴한다. 그리고 autorelease가 된다. 그럼, autorelease는 자동으로 release해주는 것인가? 아니면, 조금 있다가 release시켜주는 것인가?
autorelease를 이해하기 위해서는 Cocoa Framework에 있는 NSAutoreleasePool이라는 클래스를 알 필요가 있다. NSAutoreleasePool은 객체를 가지고 있다가 풀이 소멸될 때 가지고 있던 객체들에게 -release메세지를 보내는 역할을 한다. 즉, autorelease를 사용하면 당장 release되지 않고 NSAutoreleasePool이 소멸될 때 release되는 것이다. 이렇게 함으로써 객체가 얼마간 소멸되지 않도록 해준다. 다음의 코드를 보자.


pool = [[NSAutoreleasePool alloc] init];


anObject = [[NSObject alloc] init];
NSLog(@"%d", [anObject retainCount]);

[anObject retain];
NSLog(@"%d", [anObject retainCount]);

[anObject release];
NSLog(@"%d", [anObject retainCount]);

[anObject autorelease];
NSLog(@"%d", [anObject retainCount]);

[pool release];


이 결과는 1, 2, 1, 1이 나오고, 결국 -autorelease가 불리더라도 retainCount가 감소되지 않는다는 것을 알 수 있다. 하지만, pool이 release되어 소멸되는 순간 anObject도 release되기 때문에 이때 anObject는 소멸하게 된다.
NSAutoreleasePool객체는 Cocoa Application에서는 항상 존재하며, 만약 하나도 없으면 객체가 생성될 때 런타임 워닝이 발생한다. 소멸되어야 할 객체가 소멸되지 않을 것이라는 이야기다. Cocoa Framework에 존재하는 대부분의 객체들은 이 NSAutoreleasePool을 사용하므로, Cocoa Framework의 객체를 하나도 사용하지 않는 Objective-C프로그램을 만든다면 굳이 NSAutoreleasePool을 사용하지 않을 수 있겠지만, Mac OS X을 이용한다면 아마도 절대 그런 일은 없을 것이다.
NSAutoreleasePool은 어플리케이션에서 하나 이상 존재하게 된다. 만약 NSAutoreleasePool객체가 어플리케이션이 시작하면서 부터 종료될 때 까지 단 하나만 존재한다면, 굳이 NSAutoreleasePool을 사용할 필요가 없을 것이다. 어차피 어플리케이션이 종료되면 모든 자원이 O/S에 의해 회수될 것이니까. 여러개의 NSAutoreleasePool객체는 스택(stack)으로 존재해서 autorelease가 되는 객체들은 가장 마지막에 만들어진 NSAutoreleasePool객체에 보존된다.
일반적인 AppKit어플리케이션의 경우 Cocoa Framework에 의해 이벤트가 발생할 때마다 NSAutoreleasePool을 생성했다가 소멸시킨다. 그럼으로써 이벤트처리시 만들어지는 객체들을 자동소멸하게 되는 것이다. 만약, AppKit을 사용하지 않는 서버프로그램을 작성한다고 하면 프로그래머는 NSAutoreleasePool을 적당히 사용하여 어플리케이션이 쓸데없는 시스템자원을 차지하게 되는 현상을 막아야한다.

[편집] 6. 잠시 사용할 객체 얻기.


앞서 NSString의 객체를 생성하는 메소드들중에 +new를 이용하거나 +alloc과 -init을 연속으로 불러서 객체를 얻는 방법 외에 국적없는(?) +string이라는 메소드를 언급한 적이 있다. 이 메소드는 새로운 빈 문자열 객체를 생성해서 리턴하기 때문에 역시나 생성을 위한 메소드임에는 틀림없다. 하지만, 이 메소드가 다른 점은 객체가 리턴될 때 -autorelease가 한번 불려진다는 점이다. 즉, 잠시동안 사용할 문자열 객체에 대해서 +alloc과 -init을 이용해 생성한 다음 사용을 마치고 -release를 부르는 것이 귀찮을 정도라면 +string을 사용하라는 것이다. 그럼, 객체는 이미 NSAutoreleasePool에 추가되어서 풀이 소멸될 때 release되게 되는 운명을 가지고 생성되는 것이다.

[편집] 7. 객체를 복사하기.


이외에도 새로운 객체를 생성시키는 방법으로 이미 생성되어 있는 객체의 복사본을 얻는 방법이 있다. 복사라는 말은 객체를 하나 더 만들되 그 객체의 내용을 원본객체의 내용과 똑같게 하는 것이다. 객체를 복사하는 메소드는 -copy이다. 예를 보자.


anotherObject = [anObject copy];


anObject는 이미 존재하는 객체라고 가정하고 위의 코드는 anObject와 내용이 똑같은 새로운 객체를 생성해서 anotherObject변수에 대입한다. 객체복사가 끝난 직후에는 anObject와 anotherObject가 내용이 똑같겠지만, 결국 실제는 다른 객체이기 때문에 복사후 anObject에 변경을 가하더라도 anotherObject에 영향을 미치지는 않는다. 그러니 없던 객체를 생성한 꼴이된다.
복사된 객체는 원본객체와 같은 클래스이고, 원본객체가 가지고 있던 모든 인스턴스변수에 대입되어 있던 객체나 값들도 새로운 객체에 복사된다.
객체를 복사하는 것에서 한가지 알아야 하는 점은 -copy라는 객체 복사 메소드에 의해 리턴되는 새로운 객체는 retainCount가 1이며, 바로 위에서 봤던 +string과 같은 메소드처럼 autorelease되어지지 않는다. 즉 +new를 이용해서 생성한 것과 같다. 그러므로, 복사해서 사용한 객체도 사용이 끝나면 반드시 release해 주어야 한다는 점을 잊지 말자.

[편집] 8. 객체생성을 위한 메소드들.


아주 특별한 경우가 있을지 모르지만, 그런 경우를 제외하고 Cocoa Framework의 모든 객체는 객체생성을 위한 메소드에 네이밍룰(Naming Rule)이 있다. 이 룰을 알게됨으로써 굳이 메소드의 레퍼런스를 보지 않아도 사용자가 release해 주어야 할지 말아야할지를 알 수 있으며, 특히 객체를 디자인하고 구현하는 프로그래머의 경우는 이 룰을 따라줌으로써 다른 사용자가 좀더 편리하게 접근할 수 있게 된다.
현재까지 알아본 모든 객체생성방법과 함께 간단히 정리해 보겠다.

1.+alloc은 객체를 생성하고 retainCount를 1로 만든다.
2.-init...은 생성된 객체를 초기화하는 메소드다.
3.+new...는 +alloc과 -init...이다.
4.클래스의 이름을 따온 생성메소드(앞의 예:+string)는 +alloc, -init..., -autorelease이다. (가끔씩 아닌 경우도 있지만 중요한 점은 release해 줄 필요없다는 것이다.)
5.-copy...는 +alloc과 -init...으로 생성된 같은 내용의 객체다.

[편집] 9. Point구현하기.


여기서 우리가 배운 것들을 중심으로 Point클래스를 구현해 보도록 하겠다.


@interface MyPoint : NSObject <NSCopying>

{
NSNumber *_x;
NSNumber *_y;
}

+ (id)point;
+ (id)pointWithPoint:(MyPoint *)point;
+ (id)pointWithX:(NSNumber *)x y:(NSNumber *)y;

- (id)init;
- (id)initWithPoint:(MyPoint *)point;
- (id)initWithX:(NSNumber *)x y:(NSNumber *)y;

- (NSNumber *)x;
- (void)setX:(NSNumber *)x;

- (NSNumber *)y;
- (void)setY:(NSNumber *)y;

@end

@implementation MyPoint

- (id)copyWithZone:(NSZone *)zone {
return [[[self class] allocWithZone:zone] initWithX:[[_x copy] autorelease] y:[[_y copy] autorelease]];
}

+ (id)point {
return [[[self alloc] init] autorelease];
}

+ (id)pointWithPoint:(MyPoint *)point {
return [[[self alloc] initWithPoint:point] autorelease];
}

+ (id)pointWithX:(NSNumber *)x y:(NSNumber *)y {
return [[[self alloc] initWithX:x y:y] autorelease];
}

- (id)init {
self = [super init];
if (self) {
_x = [[NSNumber alloc] initWithFloat:0];
_y = [[NSNumber alloc] initWithFloat:0];
}
return self;
}

- (id)initWithPoint:(MyPoint *)point {
self = [super init];
if (self) {
_x = [[point x] retain];
_y = [[point y] retain];
}
return self;
}

- (id)initWithX:(NSNumber *)x y:(NSNumber *)y {
self = [super init];
if (self) {
if (!x || !y) {
[self release];
return nil;
}
_x = [x retain];
_y = [y retain];
}
return self;
}

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

- (NSNumber *)x {
return _x;
}

- (void)setX:(NSNumber *)x {
if (x) {
[_x release];
_x = [x retain];
}
}

- (NSNumber *)y {
return _y;
}

- (void)setY:(NSNumber *)y {
if (y) {
[_y release];
_y = [y retain];
}
}

@end


참고로 Point는 NSCopying protocol을 구현하고 있다. -copyWithZone:은 NSCopying프로토콜에 의한 것이다. -copy는 NSCopying프로토콜의 -copyWithZone:메소드를 사용하므로 Point에서 굳이 -copy를 구현할 필요는 없다. 특히 주의해서 봐야 할 것은 다음의 차이이다.

point1 = [[Point alloc] initWithPoint:aPoint];
point2 = [aPoint copy];


두 point1과 point2는 내용은 같지만, 사실은 좀 다르다. -initWithPoint:와 -copyWithZone:의 구현상 차이를 잘 살펴보라. 혹시 좀 더 자세한 내용을 알고 싶으면 Foundation Framework의 NSCopying프로토콜 레퍼런스를 읽어보기 바란다.

그리고, 마지막으로 -initWithX:y:메소드를 잘 살펴보기 바란다. 알아두어야 할 사실은 -init...메소드가 생성된 객체를 초기화할 때 만약 어떤 이유에 의해서 초기화를 할 수 없다면 nil을 리턴한다는 사실이다. 만약 super의 -init...이 실패해서 nil이 리턴된다면 초기화과정을 거치지 않고 바로 리턴한다. 혹시 이 경우 초기화를 하기위해 객체를 생성하게 되면 이때 생성된 객체들은 영영 소멸되지 않게 된다. 또한, 초기화과정에 있어서도 실패를 하게 되면 nil을 리턴하는데, 이때는 반드시 release해야한다는 것을 잊지 말자. release하지 않는다면, 역시나 이 객체는 영영 소멸하지 않게 된다.

좀더, 완벽한 Point객체를 구현하기 위해서는 여러가지 예외처리가 필요할 것이고 더 복잡해 지겠지만, 여기서는 생성과 소멸문제를 집중적으로 다뤘기 때문에 이 정도만 해도 충분하리라고 생각한다. 부족한 글이지만 약간이나마 도움이 되어 더 나은 프로그램을 만들수 있게 되기를 바란다.