ARC에 대한 정리를 하다가 WWDC 2021
에 ARC 관련 Session 을 찾게 되어서 정리하게 되었습니다.
기존에 알고 있던 것들에서 어떤 것들을 새롭게 알게 되었는지 정리하겠습니다.
객체의 생명주기는 위와 같이 ARC와 연관되어서 정해집니다.
- 객체의 이니셜라이져를 통해서 생명주기가 시작되고 그 객체의 사용이 끝나는 시점에 생명주기가 종료
- ARC가 생명주기가 끝난 객체를 deallocate(할당 해제)함
- ARC가 reference count를 활용해 객체의 생명주기를 추적
- Swift 컴파일러가 retain / release 연산자를 삽입
- Swift가 런타임에서 reference count가 0인 값들을 할당 해제
예시를 통해서 알아보도록 하죠
여행을 위해서 목적지를 만드는 App을 만든다고 생각하고 생각해보겠습니다.
Traveler 객체는 class 이므로 참조 타입입니다.
test 메서드의 동작을 보면 아래와 같은 순서대로 진행될 것입니다.
- Traveler 인스턴스 생성
- 인스턴스에 대한 참조
- 인스턴스에 대한 값 변경
그렇다면 컴파일러에서는 어떤 일이 일어났을지 보겠습니다.
변수 traveler1
은 Traveler 인스턴스가 생성되는 시점에 참조하기 시작했고, traveler2
가 traveler1
을 복사한 이후에 traveler1
에 대한 사용은 끝이 났습니다.
Swift 는 그럼 객체의 사용이 끝난 그 시점에 release
를 삽입해서 참조를 끝냈을 겁니다.
그럼 traveler2
의 참조 과정에 대해서 알아보겠습니다.
traveler2
의 경우는 일단 init 을 통해서 생성된 인스턴스를 통해 시작하는 것이 아니다 보니 retain
이 삽입 될 것입니다.
그리고 마지막으로 사용되는 부분에 release
가 역시나 삽입 될 겁니다.
이러한 일련의 과정들을 메모리 관점에서 알아보겠습니다
우선은 Traveler
타입이 클래스 타입이다보니 Heap 영역에 생성됩니다
인스턴스 생성 당시에는 retain
없이도 Reference Count가 1일 것입니다.
traveler2
를 위해서 retain
이 삽입되고 이를 통해서 Reference Count는 2로 증가합니다.
특이한 점은 retain
의 경우는 traveler2
가 명시되기 이전에 삽입되고 release
는 사용이 끝난 다음에 삽입된다는 것입니다!
그리고 그 이후에는 이렇게 release
를 통해서 Reference Count가 줄어들어 0이 되고 메모리에서 해제되는 것을 볼 수 있습니다.
결국 참조 타입 객체는 이니셜라이저를 통해서 할당이 시작되며, 더 이상 사용되지 않을 경우에 메모리에서 해제됩니다.
이러한 Swift ARC의 특징은 다른 언어들과는 조금 다르다고 하는데, 다른 언어들은 괄호가 닫힐 때까지 보장된다고 합니다.
Swift 의 경우 항상 사용이 끝나는 시점에서 해제되는 것은 아니며, 정확하게 그 시점을 알아내기는 애매한 것 같습니다. ARC의 최적화에 따라 그 정도가 정해지는 것 같습니다.
앞에서 ARC에 의한 참조 타입 인스턴스의 생명주기에 대해서 대략적으로 알아봤습니다. 대부분의 사람들이 알고 있던 내용이였습니다. 하지만 다음 내용들은 조금 생소했는데, 우리가 순환 참조 문제를 해결하기 위해 사용했던 방법들과 ARC의 동작 때문에 생길 수도 있는 잠재적 버그들에 대해서 소개합니다.
다음 코드는 이전에 사용했던 Traveler
타입과 여행자의 계정을 나타내는 Account
타입이 추가된 코드입니다.
대략적으로 봐도 알겠지만... Traveler
과 Account
가 서로 인스턴스 프로퍼티를 통해서 참조하고 있습니다. 그렇습니다... 순환참조 문제가 발생하는 경우입니다.
메모리 누수가 발생하는 경우죠...
우리는 이러한 문제를 한 두번 겪은게 아닙니다. 그래서 다들 알다시피 약한 참조 혹은 미소유 참조를 통해서 이러한 문제들을 해결해왔습니다.
Account
의 프로퍼티 참조 형태를 weak 로 변경시킴으로써 이제 메모리 누수가 발생할 가능성이 사라졌습니다.
하지만, 여기서 문제가 발생할 수 있는데, test()
메서드에서 Traveler
인스턴스는 traveler.account = account
문장에서 사용이 끝나서 release
가 삽입되어 메모리 해제될 수도 있습니다.
그럴경우, printSummary
에서 !
옵셔널 강제 언래핑 해놓은 저 코드로 프로세스 자체가 종료될 수도 있습니다.
크래쉬가 나는 것을 방지하기 위해서 옵셔널 바인딩을 한다고 해도 명확하게 알아차리기 힘든 버그가 생기는 것입니다.
이를 처리하기 위한 방법들을 해당 Session에서 소개해주고 있습니다.
Swift 에서 명시적으로 특정 인스턴스의 생명주기를 연장할 수 있도록 withExtendedLifetime
이라는 유틸리티를 제공하고 있습니다.
이렇게 사용할 경우에 끝까지 Traveler
인스턴스의 생명주기가 해당 블록이 끝날 때까지라고 확신할 수 있습니다.
하지만 Weak Reference
가 버그를 일으킬 가능성 있을 때마다 해당 함수를 통해서 관리한다는 것은 엄청나게 피곤해보입니다... 가독성 측면에서도 보기 싫구요
애초에 이러지 않도록 재설계하는 것이 더 나은 선택입니다.
생각해보면 Account
클래스의 경우는 Traveler
클래스를 직접적으로 참조할 필요가 없습니다. 단지, Traveler
의 이름만 필요한 것입니다.
PersonalInfo
라는 타입을 새롭게 생성하고 여기에 name을 담는다면 순환 참조하는 구조에서 벗어날 수 있게 됩니다.
이렇게 할 경우, 새롭게 객체를 생성하는데 있어서 추가적 비용이 발생하기는 하지만, 잠재적 버그를 제거함과 동시에 확장성 측면에서 더 좋은 코드가 된 것 같습니다.
이제껏 아무 걱정 없이 weak
참조를 사용해오던 입장에서 버그 가능성에 대해서 알게되니... 정말 새로운 경험이였습니다.
앞에 내용과 비슷한 맥락이긴 한데, 끝까지 소개해보도록 하겠습니다. Swift 의 Class 타입의 경우 인스턴스에 대한 메모리 해제가 있는 시점에 동작할 수 있도록 하는 기능이 있습니다. 바로 deinit
입니다.
일반적인 경우 Traveler의 deinit 메서드는 Done traveling
보다 먼저 호출됩니다. 하지만 ARC 최적화가 작동하면 메서드 마지막에 호출될 수도 있다고 합니다.
그러니 사실상 컴파일러의 판단이기에 우리가 확정지어서 말할 수는 없습니다. 순서가 중요한 로직이라면 버그가 발생할 수 있을 겁니다.
Traveler
객체가 deinit 되면 publish를 호출합니다.
publish
는 traveler의 id, 조회한 destination 수, 관심 카테고리를 나타냅니다.
deinit
은 관심도를 계산한 후 실행되어 관심 카테고리를 Nature
로 publish 할 수 있습니다.
하지만 Traveler
객체의 마지막 사용은 업데이트가 끝나는 시점이기에, 그 후에 computeTravelInterest
를 실행하지 않고 deinit
을 실행할 수도 있습니다.
그렇다면 아까처럼 withExtendedLifetime
를 사용하거나 재설계
를 통해서 해결할 수 있습니다.
ARC의 동작에 대해서 알고 있던 내용들을 확인받게 된 좋은 계기였으며, 순환 참조에 대한 해결 방법이라고 알고 있던 weak, unowned
참조가 버그 가능성을 가지고 있다는 것을 알게 된 좋은 계기였습니다.
블로그 잘보고 있습니다!