스위프트는 ARC(Automatic Reference Counting)를 사용하여 앱의 메모리 사용량을 관리하고 추적한다. Swift를 효율적으로 사용하기 위해서 ARC가 동작하는 방식을 이해 해보자.
Object lifetimes and ARC
Object lifetime
은init()
에서 시작되고 마지막으로 사용되면 끝난다.- ARC는 lifetime이 끝난 객체를 deallocate(할당 해제)한다.
- ARC는 참조 카운트(reference count)를 사용하여 객체의 lifetime을 추적한다.
- Swift 컴파일러가
retain
과release
를 자동으로 삽입한다. - reference count가 0이 되면 객체는 deallocate된다.
위의 예제코드에서 Traveler
의 참조가 시작되고 끝나는 지점을 확인할 수 있다.
traveler1
이 Traveler
의 인스턴스를 생성하는 시점에 참조가 시작되고, traveler2
가 traveler1
을 참조하는 시점에 traveler1
에 대한 사용이 종료되어 참조가 종료된다.
Swift 컴파일러는 init()되는 시점에 reference count가 1 올라가고(retain
생략),
traveler1
에 대한 사용이 끝난 지점에 release
를 자동으로 삽입해 준다.
그렇다면 traveler2
인스턴스에 대한 참조는 어떻게 될까?
traveler2
가 traveler1
을 참조하는 시점에 traveler1
에 대한 참조가 시작되고, traveler2
의 destination에 “Big Sur"를 할당하는 시점에 traveler1
에 대한 참조가 종료된다.
Swift 컴파일러가 시작 전 retain
을 삽입하고, traveler2
의 마지막 사용(destination에 “Big Sur"를 할당)후에 release
를 삽입해 준다.
런타임에서 무슨 일이 일어나고 있는지 그림으로 다시 확인해 보자.
Traveler
객체는 초기화되면서 Heap
영역에 생성되고, reference count가 1로 증가한다.
traveler1
을 traveler2
에 할당하면서 traveler1
의 reference count가 2로 증가한다.
treveler1
의 사용이 완료되어 release
가 호출되면서 reference count가 1로 감소한다.
traveler2
의 destination
에 “Big Sur"를 할당되어 계속해서 treveler2
가 사용되고 있으므로 reference count는 1로 유지된다.
traveler2
의 사용이 완료되어 release
가 호출되면서 reference count가 0이 되고 메모리에서 해제된다.
object lifetime in Swift
스위프트에서 객체의 lifetime은 사용기반(use-based)으로 결정된다. 객체의 lifetime은 초기화되는 시점부터 마지막 사용까지 보장된다.
실제 수명 주기는 컴파일러가 자동으로 삽입해주는 retain
과 release
에 의해 결정되므로, 최소한으로 보장된 수명주기와 달라질 수 있다.
Observable object lifetimes
대부분의 경우 객체의 정확한 수명은 중요하지 않을 수 있다.
하지만 weak
, unowned
를 사용하거나 Deinitializer side effects
와 같은 기능을 사용하는 경우 객체의 수명을 관찰해야 할 필요가 있다.
생성된 객체가 해제되지 않고 오랫동안 남아있을 가능성도 있고, 컴파일러가 업데이트 되거나 소스코드가 변경되면서 문제가 발생할 가능성이 있다.
weak and unowned references
weak
와 unowned
참조는 reference count를 증가시키지 않는다. 따라서 보통 reference cycle을 방지하기 위해 사용한다.
reference cycle 참조 사이클?
위의 Traveler
와 Account
클라스를 살펴보자,
Traveler
는 account 프로퍼티를 통해 Account
를 가지고 있고, Account
는 traveler 프로퍼티에서 Traveler
를 가지고 있다. 서로가 서로를 참조하고 있기 때문에 reference cycle이 발생한다.
아래 그림과 같이 test
함수를 통해 Traveler
타입과 Account
타입의 인스턴스를 생성하고 lifetime을 살펴보자.
Heap 영역에 Traveler
타입 인스턴스가 생성되며 reference count는 1로 올라간다.
다음으로 account 프로퍼티를 통해 Account
타입 인스턴스가 생성되며 reference count는 1로 올라간다. account는 Traveler
타입을 참조하으로 Traveler
인스턴스의 reference count는 2로 증가한다.
test함수의 3번째 라인을 통해 Traveler 타입의 account 프로퍼티에 Account
인스턴스를 할당한다. 이때 Account
인스턴스의 reference count는 2로 증가한다.
Account
타입의 마지막 사용이 끝났기 때문에 Account
인스턴스의 reference count는 1로 감소한다.
test함수의 4번째 라인을 통해 Traveler
타입의 마지막 사용이 끝났기 때문에 Traveler
인스턴스의 reference count 역시 1로 감소한다.
모든 사용이 다 종료 되었지만, Traveler
인스턴스와 Account
인스턴스는 reference count가 0이 되지 않고 계속해서 1로 남아있기 때문에 메모리에서 해제되지 않고 계속해서 남아있으면서 메모리 누수(memory leak)을 발생시킨다. 이러한 상황을 reference cycle
이라고 한다.
weak, unowned references
reference cycle
을 해결하기 위해 weak
와 unowned
참조를 사용할 수 있다.
메모리에서 해제된 인스턴스를 참조하려고 하면 weak
reference 로 선언된 객체는 nil
을, unownded
reference로 선언된 객체는 trap
을 반환한다.
위의 예제에서 Account
타입의 traveler 프로퍼티를 weak
로 선언하면 Traveler
인스턴스의 reference count는 0이 되어 메모리에서 해제된다.
Traveler
인스턴스가 해제되면서 acount 프로퍼티에 할당된 reference도 해제되기 때문에 Account
인스턴스의 reference count는 0이 되어 메모리에서 해제된다.
지금까지는 weak
를 참조를 끊어주는데(break) 사용했지만, 만약 weak
를 객체에 접근할 때 사용한다면 오류를 발생시킬 수 있다.
printSummary
메서드는 Account로 이동하였다.
test()
함수의 traveler.account = account
에서 traveler 인스턴스가 마지막으로 사용되고 메모리에서 해제되면 account 인스턴스는 traveler 인스턴스에 접근할 수 없게 된다.
Reference
WWDC21 - ARC in Swift: Basics and beyond
Swift Doc - Automatic Reference Counting