Objective-C의 레퍼런스 카운트는 왜 필요한 건가
다른 언어를 사용하던 이가 Objective-C 를 접했을 때 가장 생소해하며 혹은 헷갈려 하는 부분이 바로 레퍼런스 카운트 개념이다. 이 글은 레퍼런스 카운트 개념이 왜 필요한지에 대한 것이다. 다만 도움이 될지 이해가 될지 잘 모르겠다. ;-)
레퍼런스 카운트
레퍼런스 카운트(Reference Count)는 Objective-C의 가장 독특한 특징이다. 메모리를 할당 할 때, 혹은 해당 메모리 포인터를 참조 할 때 레퍼런스 카운트를 증가시킨다. 사용이 완료되었다면 레퍼런스 카운트를 감소시키도록 한다. 그리고 레퍼런스 카운트가 0이 되는 시점에 메모리가 해제된다.
전통적인 메모리 할당/해제 방식은 alloc과 free의 페어(pair)이다. 즉, alloc으로 메모리가 할당되고 free로 메모리가 해제된다. 굉장히 직관적이라고 볼 수 있다.
이런 전통적인 메모리 관리 개념과 비교할 때 Objective-C의 레퍼런스 카운트 개념은 불편하고 이해가 안되는 요소이다.
하지만 레퍼런스 카운트 개념은 오히려 편리성을 위해서 만들어진 개념이다.
왜 레퍼런스 카운트가 필요한가
이해 못 하는 분이 계실지도 모르겠지만, 약간의 C코드 예제를 들어본다
이 코드 만으론 깔끔하다. 하지만 문제를 조금 더 확장시켜보자.
만약 some_func_1() 이라는 함수 내부에서 특정 조건에서 buffer를 free시키는 코드가 들어있다고 생각해보자. 버그라고도 생각 할 수 있겠지만 일부러 그렇게 만들어야 되는 케이스가 없다고는 할 수 없다.
만약 some_func_1() 에서 buffer의 메모리가 해제되어 버리면 그 아래의 some_func_2() 함수와 free() 두 쪽에서 문제를 일으킨다 이 경우 발생하는 오류가 SIGSEGV. 똥(core)싸고 죽는다. -_-;;
레퍼런스 포인터 개념은 이런 코드 디자인에서 빛을 발휘한다. 만약 레퍼런스 포인터 개념이 있다면 위 코드는
some_func_1()과 some_func_2()의 마지막에는 해당 메모리를 더 이상 참조할 필요가 없으니 레퍼런스 카운트를 감소시킨다. 이 개념이 릴리즈(release)이다.
코드는 약간 더 늘어나겠지만, 이렇게 레퍼런스 카운트 개념을 이용하면 while 루프 내부에서 buffer의 메모리가 해제될 수가 없다. 그래서 some_func_1()의 행동과는 관계 없이 some_func_2()도 안전하게 buffer를 엑세스 할 수 있게 된다.
물론 이와는 다르게, 전통적인 방식대로 buffer를 free시키고 buffer를 NULL로 초기화 해 주고, 각 buffer를 참조하는 루틴은 NULL을 체크하면 최소한 죽는 문제는 해결 할 수 있다. 하지만 이렇게 해 버리면 오동작이 발생하는 것과 다를 바 없다.
(... 물론 애초에 some_func_1()이 메모리를 해제시키는 것 자체가 문제긴 하다 ...)
정리
레퍼런스 카운트 개념은 코딩에서는 불편한 존재가 될 수 있다. 간혹 복잡하다고 이해를 못 할 수도 있고 간혹 제대로 릴리즈를 하고 있는지 체크하는 것이 불편할 때도 있다.
하지만 이로 인해 얻는 잇점은 메모리 관리의 효율성이다.
그런데 좋은 점을 말해봐서 뭐 하나. 어차피 레퍼런스 카운트도 제대로 코딩해야 제대로 동작할 뿐이다. 어차피 전통적인 방식도 제대로 코딩했다면 문제될 것이 없다.
이 글은 레퍼런스 카운트 방식이 좋다는 글이 아니다. 그저 잘 쓰면 좋은 것일 뿐이다.
관련 포스트: Xcode ARC, 약인가 독인가
레퍼런스 카운트
레퍼런스 카운트(Reference Count)는 Objective-C의 가장 독특한 특징이다. 메모리를 할당 할 때, 혹은 해당 메모리 포인터를 참조 할 때 레퍼런스 카운트를 증가시킨다. 사용이 완료되었다면 레퍼런스 카운트를 감소시키도록 한다. 그리고 레퍼런스 카운트가 0이 되는 시점에 메모리가 해제된다.
전통적인 메모리 할당/해제 방식은 alloc과 free의 페어(pair)이다. 즉, alloc으로 메모리가 할당되고 free로 메모리가 해제된다. 굉장히 직관적이라고 볼 수 있다.
이런 전통적인 메모리 관리 개념과 비교할 때 Objective-C의 레퍼런스 카운트 개념은 불편하고 이해가 안되는 요소이다.
하지만 레퍼런스 카운트 개념은 오히려 편리성을 위해서 만들어진 개념이다.
왜 레퍼런스 카운트가 필요한가
이해 못 하는 분이 계실지도 모르겠지만, 약간의 C코드 예제를 들어본다
void *buffer = malloc(1024); while (...) { some_func_1(buffer); some_func_2(buffer); } free(buffer);굉장히 간단한 코드이다. 첫 줄에서 buffer에 메모리를 할당하고 (정확히 말해서 할당된 메모리의 포인터를 buffer라는 포인터 변수에 저장하고) while문 내부에서 이 buffer를 활용한다. while 문이 종료되면 free를 통해 buffer에 할당된 메모리를 해제한다.
이 코드 만으론 깔끔하다. 하지만 문제를 조금 더 확장시켜보자.
만약 some_func_1() 이라는 함수 내부에서 특정 조건에서 buffer를 free시키는 코드가 들어있다고 생각해보자. 버그라고도 생각 할 수 있겠지만 일부러 그렇게 만들어야 되는 케이스가 없다고는 할 수 없다.
만약 some_func_1() 에서 buffer의 메모리가 해제되어 버리면 그 아래의 some_func_2() 함수와 free() 두 쪽에서 문제를 일으킨다 이 경우 발생하는 오류가 SIGSEGV. 똥(core)싸고 죽는다. -_-;;
레퍼런스 포인터 개념은 이런 코드 디자인에서 빛을 발휘한다. 만약 레퍼런스 포인터 개념이 있다면 위 코드는
- malloc 단계에서 레퍼런스 카운트 증가(alloc). 카운트는 1
- some_func_1() 내부에서 레퍼런스 카운트 증가(retain). 카운트는 2
- some_func_1() 이 종료될 때 레퍼런스 카운트 감소(release). 카운트는 1
- some_func_2() 내부에서 레퍼런스 카운트 증가(retain). 카운트는 2
- some_func_2() 가 종료될 때 레퍼런스 카운트 감소(release). 카운트는 1
- free에서 레퍼런스 카운트 감소(release). 카운트는 0. 메모리가 해제된다.
some_func_1()과 some_func_2()의 마지막에는 해당 메모리를 더 이상 참조할 필요가 없으니 레퍼런스 카운트를 감소시킨다. 이 개념이 릴리즈(release)이다.
코드는 약간 더 늘어나겠지만, 이렇게 레퍼런스 카운트 개념을 이용하면 while 루프 내부에서 buffer의 메모리가 해제될 수가 없다. 그래서 some_func_1()의 행동과는 관계 없이 some_func_2()도 안전하게 buffer를 엑세스 할 수 있게 된다.
물론 이와는 다르게, 전통적인 방식대로 buffer를 free시키고 buffer를 NULL로 초기화 해 주고, 각 buffer를 참조하는 루틴은 NULL을 체크하면 최소한 죽는 문제는 해결 할 수 있다. 하지만 이렇게 해 버리면 오동작이 발생하는 것과 다를 바 없다.
(... 물론 애초에 some_func_1()이 메모리를 해제시키는 것 자체가 문제긴 하다 ...)
정리
레퍼런스 카운트 개념은 코딩에서는 불편한 존재가 될 수 있다. 간혹 복잡하다고 이해를 못 할 수도 있고 간혹 제대로 릴리즈를 하고 있는지 체크하는 것이 불편할 때도 있다.
하지만 이로 인해 얻는 잇점은 메모리 관리의 효율성이다.
그런데 좋은 점을 말해봐서 뭐 하나. 어차피 레퍼런스 카운트도 제대로 코딩해야 제대로 동작할 뿐이다. 어차피 전통적인 방식도 제대로 코딩했다면 문제될 것이 없다.
이 글은 레퍼런스 카운트 방식이 좋다는 글이 아니다. 그저 잘 쓰면 좋은 것일 뿐이다.
관련 포스트: Xcode ARC, 약인가 독인가
댓글