메모리 관리의 첫번째 원칙은 누군가 어떤 메모리를 할당했다면, 그 해제의 책임도 함께 져야 한다는 것이다. 예를 들어 어떤 코드에서 새로운 객체를 생성했다면, 이 객체를 해제하는 책임 역시 그 코드가 갖게 된다. 그러나 상황에 따라서는 이 원칙을 지키는 것이 여의치 않을 수 있다. 이를 테면 어떤 객체를 생성해서 다른 곳으로 전달해주어, 그 객체가 언제까지 유지되어야 할지 알지 못할 때로 함수 내부에서 생성된 객체가 그 함수의 리턴 값으로 전달된 경우를 말한다.
여기서 메모리 관리의 두 번째 원칙이 등장하는데, 필요에 의해서 어떤 객체의 수명을 연장했다면 그 해제의 책임을 함께 져야 하는 것이다. 즉 첫번째 원칙이 작동할 수 없는 경우에는 두 번째 원칙에 의해서 객체 파괴의 의무가 이양된다고 할 수 있다. 즉 코드상에서는 함수 내부에서 생성된 객체가 리턴되었다면, 그 리턴을 받는 곳 (함수를 호출했던 부분)이 해당 객체의 릴리즈 의무를 이어받게 되는 것이다.
객체가 필요할 때 생성하고 객체가 필요 없어지면 폐기한다는 것은 간단한 원칙이지만, 실제로 이를 코드 상에서 정확하게 관리하는 것은 쉽지 않다. 지금 이 객체를 파괴하는 것이 다른 곳에서 문제를 일으킬 소지가 없는 것인지 예상하기 힘들 수 있기 때문이다. 하지만 Objective-C의 메모리 관리는 C보다는 어느 정도 안전하게 작성할 수 있다. Objective-C는 기본적으로 참조 수(reference count)에 의한 메모리 관리를 하기 때문이다.
alloc ~ init*
을 통해 생성된 객체는 기본적으로 1의 참조수를 가진다. 그리고 -release
를 호출받으면 자신의 참조수를 1만큼 감소시키게 된다. 그 결과 참조수가 0이 되면 더 이상 이 객체를 사용하는 (참조하는) 곳이 없다고 판단해서 런타임이 해당 객체를 파괴하게 된다. 따라서 프로그래머는 메모리 할당을 해제하는 -dealloc
메소드를 직접 호출할 일도 없고 직접 호출해서도 안된다.
이렇게 참조수를 사용하는 메모리 관리 모델에서는 객체의 라이프사이클을 일일이 추적해야하는 프로그래머의 부담이 줄어들 수 있다. Objecitve-C에서는 이를 지원하기 위해 “오토 릴리즈 풀”이라는 기능을 제공한다. 오토 릴리즈 풀은 일종의 배열이다. 어떤 객체가 배열의 원소로 추가되면 이 객체는 해당 배열에 의해 강한 참조 1개를 가지게 된다. 따라서 객체를 만든 누군가가 더 이상 이 객체를 쓰지 않는다고 해서 release
를 호출해도 배열에 의해 유지되는 참조 덕분에 객체가 파괴되는 것이 방지된다.
오토릴리즈 풀은 -drain
이라는 메소드를 가지고 있는데, 이 메소드를 호출하면 오토릴리즈 풀에 있는 모든 객체를 풀에서 제거하고, 그 결과 모든 원소 객체들은 참조수가 1만큼 감소하게 된다. 오토릴리즈풀은 NSAutoReleasePool
이라는 클래스로 제공되는데, Objecitve-C 2.0에서는 아예 @autoreleasepool ~ @end
블럭으로 오토릴리즈 풀이 관리되는 영역을 만들 수 있다. 기본적으로 앱킷/UIKit 프로그램의 진입점은 기본 오토릴리즈 풀 블럭으로 감싸지기 때문에, 대부분의 커스텀 코드에서는 기본적으로 오토릴리즈 풀이 하나 존재한다고 가정하고 코드를 작성할 수 있다.
오토릴리즈 풀은 함수내에서 생성된 후 리턴된 객체들의 제거 의무를 개발자 대신 처리하게 되는 것이다. 따라서 코드 상에서 이러한 메모리 관리 규칙을 따르는 코드를 만들기 위해서는 다음과 같이 하고 있는지를 확인하면 된다.
+alloc
을 호출했다면, 반드시release
혹은autorelease
를 호출해주어야 한다. 만약 함수 내에서 생성하여 리턴할 객체라면-autorelease
를 호출하여 함수가 종료되더라도 객체가 유지될 수 있도록 해야 한다.[NSDate date];
나[NSBundle mainBundle];
과 같이+alloc
을 직접적으로 호출하지 않은 경우라면 이 객체는 오토릴리즈 객체이다. 그러므로 오토릴리즈 객체를 리턴하는 함수나 메소드의 이름은 대체로 리턴되는 객체의 이름으로 시작하게 된다.- 커스텀 클래스의 팩토리 메서드나, 객체를 생성하여 리턴하는 함수를 작성하는 경우에 생성한 객체에 대해
-autorelease
메소드를 호출한 결과를 리턴하도록 한다.
직접 오토릴리즈 풀을 만들어 사용해야 하는 경우
XCode를 통해 코코아 앱이나 코코아터치 앱을 작성할 때에는 프로그램의 진입점에서 기본 오토릴리즈 풀이 생성된채로 프로젝트가 생성된다. 대신 Foundation 프로그램, 즉 명령줄 프로그램을 만들어서 사용하는 경우에는 main()
함수 내부에 오토릴리즈 풀을 설치해야 한다.
그 외에도 특정한 구간 내에서 집중적으로 객체가 많이 생성되는 경우가 있다면 이 구간앞뒤로 @autoreleasepool{ }
를 붙여준다. 기본 오토릴리즈 풀은 프로그램이 종료되기 전에 오토릴리즈 객체들을 풀어주게 되므로, 프로그램이 실행 중인 상황에서는 이러한 오토릴리즈 객체들이 계속 유지된다. 따라서 오토릴리즈 객체들을 집중적으로 많이 사용하게 된다면 (큰 루프 내에서 객체를 계속 생성하는 경우) 오토릴리즈 풀을 사용하는 것이 메모리 throughput을 관리하는데 도움이 될 것이다.
NSArray *urls = @[ /* file URL들의 배열 */];
for (NSURL *url in urls) {
@autoreleasepool {
NSError *error = nil;
NSString *fileContents = [[[NSString alloc] initWithContentsOfURL:url
encoding:NSUTF8StringEncoding
error:&error]
autorelease];
/* fileContents를 사용하는 코드 */
}
}
이렇게 로컬 오토릴리즈 풀을 사용하면서 해당 객체가 오토릴리즈 범위를 벗어나서도 살아있다고 유지하고 싶다면 명시적으로 retain
하면된다. 이렇게 명시적으로 retain
한 객체에 대해서는 오토릴리즈 범위 밖에서 다시 release
나 autorelease
메시지를 보내서 안전하게 해제되도록 하면 된다.
또한 오토릴리즈 풀 내의 오토릴리즈 풀이 있는 경우, 내부의 오토릴리즈 풀은 암묵적으로 오토릴리즈 객체이다. 따라서 바깥 범위의 풀이 해제될 때, 내부의 모든 오토릴리즈 풀들도 같이 해제된다.
ARC
Apple이 설계한 이러한 메모리 관리 정책은 수년간 개발자들에 의해 사용되고 검증되어 왔다. 이 정책을 신뢰하는 애플은 컴파일러의 발전에 더해서 메모리 관리 코드를 컴파일러가 직접 자동으로 삽입하도록 하는 Auto Release Count 시스템, ARC를 제공한다. (Swift는 거의 완성된 형태의 ARC를 제공하기 때문에 별도의 메모리 관리 코드 자체가 아예 필요 없는 언어가 되었다.) 이 기능은 기본적으로 alloc/init
만 존재하는 코드에서 특정한 함수 내에서 객체가 생성됐다면, 해당 함수가 리턴하는 시점에는 생성된 객체가 릴리즈되도록 자동으로 코드를 삽입한다. 만약 생성된 객체가 리턴되어야 한다면 release
대신 autorelease
를 호출하도록 한다.
컴파일러가 매우 똑똑해지면서 이러한 것들이 가능해졌지만, 언제든 상상하기 힘든 예외적인 경우는 있을 수 있다. ARC는 프로젝트 설정에서 사용되도록 옵션이 켜져 있어야 하며, 코드내에서는 프로그래머가 직접 메모리를 관리하는 부분이 없어야 한다. 즉 직접 release
나 autorelease
를 호출하는 코드를 작성했다면 ARC는 작동하지 않을 것이다
ARC도 무조건 만능은 아니며, 몇 가지 약점도 가지고 있다. 특히 꼬리재귀 형태의 함수가 있을 때, ARC는 꼬리재귀 최적화를 수행하지 못하도록 한다. 꼬리 재귀 최적화 대상의 경우, 재귀호출의 리턴값을 그대로 리턴해야 하는데 ARC가 개입하는 경우, 이 형태를 유지하지 못하게 되기 때문이다.