오늘은 이름부터 멋드러지는 ARC에 대해서 공부하고자 합니다 :)
1. ARC란
ARC란 Automatic Reference Counting의 준말로써, 자동 참조 카운트라는 의미입니다.
ARC가 있기 때문에 개발자는 별도로 메모리 관리를 위해서 인스턴스들의 참조를 직접 관리하며 해지해주지 않아도 됩니다.
무슨 말이냐면..!
모든 기기에는 메모리가 존재하고 이러한 메모리를 적절히 사용하여 작업을 처리해야합니다. 앱에서도 마찬가지로 앱의 메모리 사용을 관리해야 하는데, Swift에는 고맙게도 이를 알아서 관리해주는 ARC라는 녀석이 존재하는 것입니다!
ARC는 자동으로 참조 횟수를 관리하기 때문에 ARC가 알아서 더 이상 사용하지 않는 인스턴스를 메모리에서 해지합니다. 때문에 개발자가 메모리 관리에 신경 쓸 필요가 없다는 아주 큰 장점이 있습니다.
* 참조 횟수(Reference Counting)는 클래스 타입의 인스턴스에만 적용되며, 값 타입인 구조체/열거형 등에는 적용되지 않습니다.
2. ARC는 언제부터 존재했나
WWDC2011에서 ARC는 처음 등장하게 됩니다. Swift가 2014년도에 등장했으니 이 당시에는 Obj-C를 사용하고 있을 때였고, 따라서 ARC가 등장하기 전까지 Obj-C에서는 ARC가 아닌 MRC를 사용하고 있었습니다. Manually Reference Counting, ARC와 맥락은 같지만 그 역할을 하는 주체가 개발자인 점에서 차이가 있습니다. MRC에서는 개발자가 직접 인스턴스를 release해주어 메모리를 관리해주어야 했습니다(아구 귀찮아).
이러한 배경 때문에 기존 MRC를 사용중이다가 ARC가 등장한 Obj-C에서는 ARC의 사용을 선택할 수 있습니다(Xcode5 이후부터는 ARC가 default로 설정되어있긴함). 하지만 Swift에서는 ARC의 사용이 필수적이랍니다!
3. 그럼 어떻게 RC(Reference Counting)을 하는데?
이 질문에 앞서, ARC는 Run-time에 실행될까요? Build-time에 실행이 될까요?
계속해서 지켜보면서 사용을 안하는 애가 생기면 release해줘야 하니 Run-time 아닐까?
.
.
.
ARC는 Run-time에 계속 실행중이지 않고, Build-time에 한번만 실행됩니다.
그렇다면 어떻게 동적으로 실행되는 것들의 Reference Counting을 하고 메모리를 관리할 수 있을까요?
ARC는 compile time에 자동으로 retain, release 메서드를 적절한 위치에 삽입하는 방식으로 동작합니다.
이런식으로 complie time에 삽입이 이루어지면, runtime때 인스턴스의 생성, 참조가 일어날 때 마다 메서드가 실행되며 자동으로 Retain Count가 이루어지는 방식입니다. 그러다가 Count가 0이 되면 deinit을 통해 메모리가 해제됩니다.
Retain Count(RC)는 인스턴스의 생성 시 +1이 되고 해당 인스턴스가 참조될 때 +1이 되며, 반대로 참조가 해제되면 -1이 됩니다.
다음 글에서 설명하겠지만 strong이 아니라 weak으로 참조할 경우 RC는 증가하지 않습니다.
ARC는 사용 가능성이 있는 인스턴스의 메모리를 해제하는 일이 없도록 하기 위해 얼마나 많은 프로퍼티, 상수, 혹은 변수가 그 인스턴스를 참조하고 있는지 추적합니다. 그렇기 때문에 ARC에서는 해당 인스턴스에 대한 참조가 최소 하나라도 존재한다면 그 인스턴스를 메모리에서 해지 하지 않습니다!
+)
프로세스가 차지하고 있는 메모리를 크게 Code, Data, Heap, Stack으로 구분하는데 아까 글 서두에서 말씀드린 것 같이
* 참조 횟수(Reference Counting)는 클래스 타입의 인스턴스에만 적용됩니다. 클래스는 참조형 자료이죠? 따라서 참조 횟수는 Heap영역에서 카운팅이 됩니다!
4. ARC와 GC의 차이점
다른 언어를 접해보신 분들이라면 가비지 컬렉션(Gabage Collection, 이하 GC)에 대해서 들어보셨을겁니다. GC는 그럼 ARC와 어떤 차이가 있는걸까요? 이에 대한 질문을 얼마전에 받았었는데..대답을 하지 못했어서 ARC에 대한 포스팅인 만큼 함께 정리해보려고 합니다!
결론부터 이야기 하자면 가장 중요한 차이는 실행 시점입니다. GC는 Run-time, ARC는 Build-time시점에 실행 되죠.
GC
- Run-time: 어플 실행 중 주기적으로 참조를 추적하여 사용하지 않는 인스턴스를 해제
- 개발자가 참조 해제 시점을 파악할 수 없으며, 런타임 시점이다보니 계속 추적을 해야해서 쌓이는 오버헤드로 성능 저하 우려
ARC
- Build-time: 컴파일 시점에 retain, release 메서드를 적절한 위치에 삽입하고 런타임에 실행되게 함
- 개발자가 참조 해제 시점을 파악할 수 있고, 런타임 시점에 추가적으로 발생하는 오버헤드가 없음
- 하지만 순환 참조가 발생하면 영구적으로 메모리 해제가 되지 않기에 메모리 누수에 주의해야함
5. Strong, Weak, Unowned
위 3가지는 참조 방식의 종류입니다. ARC와는 뗄 수 없는 용어입니다. 위에서 언급한 순환 참조와도 관련된 아이구요!
이 부분은 간결하게 정리하고 넘어가도록 하겠습니다.
Strong
- 해당 인스턴스에 대한 소유권을 갖는다.
- 자신이 참조하는 인스턴스의 RC를 증가시킨다.
- 인스턴스 생성시 & 참조시 retain, 참조 종료시 release
- weak, unouwned로 선언하지 않았다면 default로 strong
- IBOutlet에서는 default가 weak
* Apple이 2015년에 강한순환참조를 피할 때를 제외하고는 strong을 사용하는게 더 안전하다고 했다고 한다..참고
Weak
- 해당 인스턴스에 대한 소유권을 가지지 않는다. 주소값만 가진다.
- 그래서 RC도 증가하지 않고 release도 일어나지 않는다.
- Optional 타입으로 nil이 될 수 있다.
* weak을 통해 강한순환참조를 해결할 수 있다!!
Unowned
- weak이랑 동일하지만 절대 nil이 될 수 없다는 점이 다름(Optional이 아니란 소리!)
- 안전하지 않음(비추)
6. 직접 해보기
간단하게 ARC 테스트 코드를 작성해보았습니다. 이해에 도움이 되었으면 좋겠네요 :)
그리고 요건 WWDC21에 나온 ARC 동작에 관한 세션
오늘도 읽어주셔서 감사합니다 :) 글에 잘못된 부분이 있다면 댓글로 꼭 알려주세요!
도움을 준 글
- The Swift Language Guide-ARC
- [Swift] ARC 뿌시기
- 타 언어의 GC와 Swift ARC의 차이
- Reference count in ARC-stackoverflow