오늘은 ARC에 대해 알아보자.
공식문서를 기준으로 작성한다.
Swift는 ARC (Automatic Reference Counting)을 이용해 메모리 관리를 한다.
이름에서 알 수 있듯 자동으로 참조 횟수를 관리하며 동작한다.
따라서 개발자는 메모리 관리에서 해방되어 코드를 작성하는데에만 집중할 수 있고, ARC가 알아서 더이상 사용하지 않는 인스턴스를 메모리에서 해제한다.
참조 횟수(Reference Counting)은 클래스 타입의 인스턴스에만 적용되고 값 타입인 구조체, 열거형 등에는 적용되지 않는다.
즉, ARC는 클래스 타입의 인스턴스만 관리한다.
클래스 인스턴스가 만들어질 때마다 ARC는 인스턴스를 저장하기 위한 메모리를 할당한다.
(이 메모리는 인스턴스에 대한 정보와 관련된 저장 프로퍼티 값들도 가지고 있다.)
인스턴스가 더이상 사용되지 않을 때 ARC는 인스턴스가 차지하고 있는 메모리를 해제한다.
이때의 '더이상 사용되지 않는다'의 기준은 참조 횟수이다.
ARC에서는 아직 사용중인 인스턴스를 해제하지 않기 위해 얼마나 많은 프로퍼티, 상수 혹은 변수가 인스턴스에 대한 참조를 갖고 있는지 추적한다.
최소 하나라도 인스턴스에 대한 참조가 있는 경우 인스턴스를 메모리에서 해제하지 않는다.
class Test {
let testMessage: String
init(testMessage: String) {
self.testMessage = testMessage
print("\(testMessage) init")
}
deinit {
print("\(testMessage) deinit")
}
}
var test1: Test?
var test2: Test?
var test3: Test?
test1 = Test(testMessage: "1")
test2 = test1
test3 = test1
test1 = nil
test2 = nil
test3 = nil
// 출력
1 init
1 deinit
인스턴스가 생성/해지 될때마다 지정된 구문을 출력하는 Test 클래스를 선언했다.
test1
, test2
, test3
은 모두 Optional 변수로서 초기값을 지정해주지 않으면 초기값으로 nil
을 갖는다.
test1 = Test(testMessage: "1")
test1
에 testMessage로 1을 갖는 Test 인스턴스를 생성했다.
testMessage로 1을 갖는 인스턴스가 생성되었으므로 메모리에 적재되므로 init()
에 작성해둔 구문이 출력된다.
test1
이 Test(testMessage: "1") 인스턴스를 참조하고 있으므로 해당 rc가 1 증가한다.
(rc: 1)
test2 = test1
test2
가 test1
즉, Test(testMessage: "1")을 참조하고 있으므로 해당 rc가 1 증가한다.
(rc: 2)
test3 = test1
test3
가 test1
즉, Test(testMessage: "1")을 참조하고 있으므로 해당 rc가 1 증가한다.
(rc: 3)
test1 = nil
test1
의 참조를 해제해 rc가 1 감소한다.
(rc: 2)
test2 = nil
test2
의 참조를 해제해 rc가 1 감소한다.
(rc: 1)
test3 = nil
test2
의 참조를 해제해 rc가 1 감소한다.
(rc: 0)
reference counting이 보로소 0이 되었다.
즉, 더 이상 참조하는 곳이 없다.
이때에 Test(testMessage: "1)은 메모리에서 해제된다.
ARC는 참조 횟수를 추적하고 더이상 참조되지 않는(사용되지 않는) 인스턴스는 자동으로 메모리에서 해제된다.
하지만 참조되지 않지만(사용되지 않지만) 메모리에서 해제되지 않는 상황이 있다.
이런 경우를 강한 참조 순환이라고 한다.
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
print("\(name) is init")
}
deinit{
print("\(name) is deinit")
}
}
class Apartment {
let unit: String
var tenant: Person?
init(unit: String) {
self.unit = unit
print("\(unit) is init")
}
deinit{
print("\(unit) is deinit")
}
}
var John: Person?
var unit4A: Apartment?
John = Person(name: "John")
unit4A = Apartment(unit: "4A")
John!.apartment = unit4A
unit4A!.tenant = John
John = nil
unit4A = nil
// 출력
John is init
4A is init
Person
, Apartment
클래스를 정의했다.
Person
클래스는 Apartment
클래스 인스턴스를 참조하는 프로퍼티를
Apartment
클래스는 Person
클래스 인스턴스를 참조하는 프로퍼티를 포함하여 작성했다.
var John: Person?
var unit4A: Apartment?
John = Person(name: "John")
unit4A = Apartment(unit: "4A")
John!.apartment = unit4A
unit4A!.tenant = John
클래스 인스턴스를 생성한 후
서로 참조하도록 프로퍼티를 지정했다.
위의 사진과 같은 참조 관계를 갖게 된다.
rc를 살펴보자.
Person instance의 rc는 2이다.
john
변수가 Person instance를 참조하고 (+1)
Apartment instance의 tenant
프로퍼티가 Person instance를 참조하고 있다.(+1)
Apartment instance의 rc 또한 2이다.
unit4A
변수가 Apartment instance를 참조하고 (+1)
Person instance의 apartment
프로퍼티가 Apartment instance를 참조하고 있다.(+1)
John = nil
unit4A = nil
John
과 unit4A
변수가 참조하고 있던 인스턴스를 nil
로 지정했다.
보통 생각엔은 변수가 참조하고 있던 인스턴스가 nil
이 되면 메모리에서 해제될 것 같다.
하지만 위의 그림 처럼 John
과 unit4A
변수의 참조는 삭제되었지만 인스턴스끼리의 참조는 남아있다.
따라서 rc가 0이 되지 않으므로 메모리에서 해제되지 않는다.
이러한 경우를 강한 참조 순환이라고 한다.
더이상 사용하지 않는 인스턴스가 메모리에 계속 남아 있다면...
이런 누수가 많다면...
앱은 금방 죽어버릴 것이다.
그렇다면 왜 강한 참조라고 할까?
별다른 지시어를 설정하지 않으면 Swift에서는 Strong이 기본이다.
강한 참조는 rc를 증가시킨다.
즉, 강한 참조는 인스턴스들을 Heap에 머물도록 하는 강한 힘이라고 할 수 있다.
위에서 알아본 강한 참조 순환을 생각해보면 Person
과 Apartment
클래스 인스턴스가 서로를 너무 강하게 잡고 있어 Heap에서 떠날 수 없는 상황이다.
그렇다면 약한 힘도 있지 않을까?라는 의문을 가지고 아래를 살펴보자.
강한 참조 순환을 해결하는데에는 weak
와 unowned
를 사용할 수 있다.
weak
와 unowned
참조는 모두 ARC에서 rc를 증가시키지 않고 인스턴스를 참조한다.
weak
는 참조하고 있는 인스턴스가 먼저 메모리에서 해제될때 사용하고
unowned
는 참조하고 있는 인스턴스가 같은 시점 혹은 더 뒤에 해제될때 사용한다.
약한 참조는 rc를 증가시키지 않는다.
weak는 해당 객체가 nil일 수 있다.
즉, weak는 반드시 Optional이고 var 키워드와 함께 사용해야한다.
약한 참조로 선언하면 참조하고 있는 것이 먼저 메모리에서 해제되기 때문에 ARC는 약한 참조로 선언된 참조 대상이 해제되면 런타임에 자동으로 참조하고 있는 변수에 nil을 할당한다.
(ARC에서 약한 참조에 nil을 할당하면 프로퍼티 옵저버는 실행되지 않는다.)
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
print("\(name) is init")
}
deinit{
print("\(name) is deinit")
}
}
class Apartment {
let unit: String
weak var tenant: Person?
init(unit: String) {
self.unit = unit
print("\(unit) is init")
}
deinit{
print("\(unit) is deinit")
}
}
var John: Person?
var unit4A: Apartment?
John = Person(name: "John")
unit4A = Apartment(unit: "4A")
John!.apartment = unit4A
unit4A!.tenant = John
John = nil
unit4A = nil
// 출력
John is init
4A is init
John is deinit
4A is deinit
강한 참조 순환 예시 코드와 달리 Apartment
클래스의 tenant
프로퍼티를 약한 참조로 바꿔줬다.
참조 관계는 위와 같이 변경된다.
그렇다면 rc를 생각해보자.
강한 참조 순환에서는 Person instance의 rc가 2였다.
Person instance의 rc는 1이다.
john
변수가 Person instance를 참조한다. (+1)
Apartment instance의 tenant
프로퍼티가 Person instance를 참조하고는 있지만 weak
이므로 rc는 증가하지 않는다.
John = nil
unit4A = nil
John
에 nil
을 할당하면 Person instance는 메모리에서 해제된다.
Person instance의 rc는 1였는데 John
의 참조가 사라지면서 rc가 0이되기 때문이다.
Person instance가 메모리에서 해제됨과 동시에 Apartment instance의 tenant
프로퍼티가 nil
이 되었다.
약한 참조로 선언하면 참조하고 있는 것이 먼저 메모리에서 해제되기 때문에 ARC는 약한 참조로 선언된 참조 대상이 해제되면 런타임에 자동으로 참조하고 있는 변수에 nil을 할당한다.
따라서 변수(var)로 선언해야 한다.
Person instance가 메모리에서 해제됨에 따라 Person instance의 apartment
프로퍼티의 Apartment instance 참조가 사라지게 되므로 Apartment instance 또한 메모리에서 잘 해제된다.
미소유 참조는 참조 대상이 되는 인스턴스가 현재 참조하고 있는 것과 같은 생애주기를 갖거나 더 긴 생애 주기를 갖기 때문에 항상 참조에 값이 있다고 기대한다.
따라서 ARC는 미소유 참조에는 nil을 할당하지 않는다.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
print("\(name) is init")
}
deinit {
print("\(name) is deinit")
}
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
print("\(number) is init")
}
deinit {
print("\(number) is deinit")
}
}
var john: Customer?
john = Customer(name: "John")
john!.card = CreditCard(number: 1, customer: john!)
john = nil
// 출력
John is init
1 - John is init
John is deinit
1 is deinit
참조 관계는 아래와 같다.
rc를 살펴보자.
Person instance의 rc는 1이다.
john
변수가 Person instance를 참조한다. (+1)
CreditCard instance의 customer
프로퍼티가 Person instance를 참조하지만 unowned
참조로 rc는 증가하지 않는다.
CreditCard instance의 rc는 1이다.
Customer instance의 card
프로퍼티가 참조한다. (+1)
john = nil
john
변수의 Person instance 참조를 해제하면 아래와 같이 참조 관계가 변한다.
Person instance의 rc는 0이 된다.
따라서 Person instance가 먼저 메모리에서 해제된다.
Person instance가 해제됨에 따라 Apartment instance의 rc도 1 감소한다.
따라서 Apartment instance도 메모리에서 해제된다.
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
print("\(name) is init")
}
deinit {
print("\(name) is deinit")
}
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
print("\(number) - \(customer.name) is init")
}
deinit {
print("\(number) - \(customer.name) is deinit")
}
}
var john: Customer?
john = Customer(name: "John")
john!.card = CreditCard(number: 1, customer: john!)
john = nil
CreditCard
의 deinit()
의 출력문을 바꿨다.
그대로 실행해보면
이와 같은 상황을 맞이할 수 있다.
unowned
참조는 nil
이 불가하다.
즉, uniowned
참조는 값이 항상 존재할거라고 생각하고 접근한다.
하지만 위의 코드에서는 john = nil
해주므로 customer
프로퍼티가 nil인 상태에서 접근하므로 크래쉬가 난다.
위에서 알아본 미소유 참조 (Unowned Reference)에 대해서 좀 더 알아보자.
이 부분은 코드를 먼저 보는게 좋을거 같다.
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
deinit {
print("Country \(name) is deinit")
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
deinit {
print("City \(name) is deinit")
}
}
var country: Country? = Country(name: "Canada", capitalName: "Ottawa")
print("\(country!.name)'s capital city is called \(country!.capitalCity.name)")
country = nil
// 출력
Canada's capital city is called Ottawa
Country Canada is deinit
City Ottawa is deinit
Swift의 클래스는 안전성을 위해 프로퍼티가 세팅되기 전까지는 사용할 수 없다.
Country 클래스의 init()
을 확인해보자.
name
프로퍼티는 세팅을 해주었는데 capitalCity
프로퍼티는 뭔가 이상하다.
City 클래스 인스턴스를 생성하는데 Country 인스턴스(self)를 사용하고 있다.
그리고 생성된 City 클래스 인스턴스를 capitalCity
프로퍼티에 세팅해주고 있다.
분명 위에서 프로퍼티가 세팅되기 전까지는 클래스를 사용할 수 없다고 했는데?!
우리가 주목해야할 부분은 var capitalCity: City!
이다.
City!
이 구문을 보면 생각나는게 있는데...
Optional에 대하여 포스팅에서 한 번 다뤘었다.
City!
는 암시적 언래핑으로 암시적으로 해당 값은 nil이 아니라고 생각한다.
따라서 nil일 수는 있지만 변수가 nil일때 접근하면 error를 받는다.
그렇다면 위의 코드에서 봐보자.
우리가 알고 있는대로라면 Country 클래스는 capitalCity
프로퍼티가 세팅되지 않아서 City 클래스 인스턴스 생성시 self를 사용할 수 없다.
하지만 capitalCity
프로퍼티의 타입을 암시적 언래핑 타입으로 선언함으로써 name
프로퍼티만 세팅해주면 Country 클래스(self) 를 사용할 수 있다.
위에서 weak
와 unowned
의 차이를 옵셔널 여부라고 말했다.
그런데 왜 뜬금없이 미소유 참조와 옵셔널?! 이라고 생각할 수 있다.
Swift5 이전까지는 unowned
에서 옵셔널이 불가했다.
Swift5부터 unowned
에 옵셔널을 지정할 수 있게 되면서 weak
참조와 같은 맥락에서 사용할 수 있게 되었다.
차이점은 unowned optional
참조를 사용할 때는 항상 유효한 객체를 참조하는지 혹은 nil로 설정되어 있는지 확인해야한다.
unowned optional
참조는 내부적으로 strong
참조를 하지 않는다.
즉, 클래스 인스턴스를 strong
하게 유지하지 않으므로 ARC가 인스턴스 할당을 해제하는 것을 방해하지 않는다.
unowned optional
참조가 nil
이 될 수 있다는 점을 제외하면 unowned
참조와 같은 동작 프로세스를 거친다.
ARC에 대해 알아보는데 넘 많다...!ㅋㅋㅋ
나눠서 정리해야겠다!
그럼 이만👋