retain/release 지옥

Apple의 세계에서 삽질/Objective-C로 삽질 2010. 9. 17. 20:12
ITObjective-C 를 시작하면서 대괄호로 인한 혼란 이후에 겪게되는 최대의 난관은 아무래도 retain/release 로 관리되는 메모리인것 같습니다. 이것들은 객체의 reference count를 증/감시키는 것인데, 이게 어찌보면 간단하면서 원시적인 메모리 관리 요령이지요(개인적으로는 반자동 관리라 부르고 있지요). 이게 때로는 매우매우 짜증나고 잡기 어려운 버그(=좀비 탄생, BAD ACCESS...--;)를 일으키는 존재라서 일각에서는 메모리 관리도 제대로 안되는 후진 플랫폼이라고 까이는 실정이지요. 저의 삽질, 그리고 나중에 들어온 H군의 삽질을 통해 알게된 점을 좀 공유해 봅니다.

retain/release 란 무엇인가?

Objective-C 객체는 내부에 retain count라는 놈을 가지고 있습니다. [object retain] 하면 retain count는 1이 증가, [object release] 하면 1이 감소...해서 retain count가 0이 되면 객체는 메모리에서 지워지지요. retain count는 어떤 다른 녀석(객체)이 어떤 객체를 가지고 있느냐...하는 ownership 비슷한 개념이라고 할 수 있겠네요. 근데 문제는 retain/release가 다른 함수(셀렉터 혹은 메시지)의 내부에서도 불리게 된다는 점이지요. 그래서 뭔가 한참 진행되고 나서는 뭐가 어떻게 되었는지 알기 어렵게 되는 상황이 비일비재합니다. 그런데...

...실은 retain/release와 관련된 대부분의 삽질과 혼란의 근원은 규칙을 잘 안지키는 것에서 일어납니다. 또 하나는 autorelease와 관련된 실수겠지요.

그럼 규칙이 뭔데?
  1. retain하면 1증가, release하면 1 감소..는 당연한 이야기
  2. alloc(후 init으로 초기화)계통의 메모리 잡는 함수는 1 증가된 녀석을 리턴(즉 retain count 1인 상태로 시작). new란 놈도 있는데 이것은 어차피 [[object alloc] init]의 축약형이므로 마찬가지지요.
  3. copy도 생성된 객체들도 마찬가지로 1 증가된 녀석을 리턴(즉 retain count 1인 상태로 시작)
  4. 그외 다른 녀석들은 모두 autoreleased object가 리턴될 것으로 가정(즉 잠시뒤까지 이 녀석이 살아있을지 어떨지는 확정지을 수 없다...그냥 너는 이미 죽어있다....라고 생각하십쇼.)
  5. 그 외에 각종 자식 객체를 갖는 놈들(view등)이나 collection 객체(set, array 이런거)에서 removeObject 비슷한 것을 부르면 remove된 객체는 release가 불립니다(가끔 이것때문에 미칩니다-오묘한 타이밍에서 뷰가 좀비가 되거나 하는 현상). 뭐 지운다는 것이 Objective-C에서는 retain count를 낮추는 것이니까...이것은 무려 Apple에서도 발생했었다고 하는 버그인 듯(특히 한 collection 에서 뽑아서 다른쪽으로 옮길 때 주의를 요함).

autorelease는 뭐지?

Objective-C의 기본적인 메모리 관리가 retain count에 의존하기 때문에 생기는 문제가 있습니다. 그에 대한 해결책(궁여지책)으로 나온 것이 autorelease라는 놈으로, release를 잠시 뒤에 하겠다는 의미입니다.  

이게 왜 필요한가...하면 바로 리턴 벨류 때문이지요. 일단 함수 등에서 리턴을 해버리면 리턴된 이후에는 리턴한 객체를 어찌해 볼 도리가 없어서 메모리 누수가 발생하게 되는데(아니면 구리게 함수 콜한 쪽에서 짝이 안맞는 release를 하는 추한 모습이 나오고...Core Foundation 시리즈에서는 좀 보임.  H군의 옛날 코드를 보면 자주 등장하기도...--;) 이를 막기 위해 궁여지책으로 좀 지연해서 release를 해 보겠다...뭐 그런 의미라고 하겠네요.

모든 어플리케이션의 시작, 이벤트(이때는 암묵적으로 프레임워크 자체에서 만들어 줌)나 쓰레드 작업이 시작될때(이 때는 명시적으로 해줘야 하는 경우가 많음) autorelease pool (NSAutoreleaePool)이란 녀석이 생긴답니다(쓰레드의 경우 때로는 직접 만들어야 됨). 객체에 autorelease 메시지를 보내면([object autorelease]) 이 객체는 release되는 대신 autorelease pool에 들어가게 됩니다. 그리고 이벤트나 쓰레드가 끝날때 autorelease pool이 release (대개는 [pool drain]을 부르지요)되면서 안에 있던 모든 객체도 다 같이 release되면서 다 같이 죽는 것이지요(물론 retain된 녀석들은 살아남지만...말이 그렇다는 겁니다).

일단 규칙을 잘 치킨다면 대부분의 경우에서 문제는 발생하지 않습니다만...

실무에서 만난 버그

메모리 누수를 관리하던 것인가...뭐 그랬을 것입니다. 근데 어떤 객체가 죽어도 삭제가 안되는 것입니다(말이 이상한가?). 제 기억엔 이 객체가 메모리 또한 엄청나게 잡아먹는 그런 녀석이었던 것으로 기억합니다. 나중에는 retain/release를 override 해서 retain count를 출력하는 등의 삽질을 한 끝에 발견한 그 원인은...

재연 나갑니다.
@interface RetainCycle : NSObject
{
	id delegate;
}

@property (nonatomic, retain) id delegate;

@end
요것만 가지고는 문제점이 다 드러나지 않지요. 실은 이 객체의 delegate 가 되는 객체가 바로 이 객체를 소유한(즉 retain하고 있는) 녀석이었다는 점. 즉 양쪽에서 서로 소유하는 형국이 되어서 한쪽이 죽기 전에 다른쪽이 죽을 수 없는 상황이 벌어진 것입니다. 이름하여...

Retain Cycle...!

통상적으로 그렇기 때문에 하위 객체에서 상위 객체를 레퍼런싱 하려 할 때라든가, delegate 경우는 retain을 하지 않는 것이 일반적입니다. 대개의 경우 상위 객체가 하위 객체를 소유하기 때문이지요. delegate도 대부분의 경우 상위 객체인 경우가 많고 더 장수하는 객체인 경우가 많으니까...만일 그렇지 않다면 님이 하신 디자인에 대해 다시 한 번 생각해 봐야 할 듯 합니다.


@interface RetainCycle : NSObject
{
	id delegate;
}

@property (nonatomic, assign) id delegate;

@end
그래서 저렇게 retain대신 assign으로 해서 문제를 해결했습니다.

요령은 없나?

규칙을 잘 지키는 것이 첫째이고, 또 하나는 Xcode에 새로 장착된 Build and Analyze를 이용해 보는 것입니다. Instruments도 필요할 때는 쓰고요.

결론

솔직히 이런 반자동 방식은 좀 병맛이랄까... 뭐 내용은 간단한데, 문제는 사람이 실수할 확률이 대단히 높다는 것. 그리고 그걸 애플식으로 기계에 사람이 맞춰가야 하는 상황이 벌어진다는 것...가비지 콜렉션이라는 것이 쉬운 것은 아니지만 적어도 사람의 실수가 날 확률이 높은 것은 문제가 될 수 있겠지요.
Posted by 타이가장관
,