오래 기다렸다. ARC가 무엇일까? 자바의 Garbage Collector와는 무엇이 다를까? 장단점은 무엇일까? 어떤 원리로 동작하는 것일까? 발생하는 문제점은 무엇일까? 어떻게 해결할 수 있을까? 이러한 내 궁금증들을 담았다. 즐겁게 읽어주길 바란다.
strong
한 참조를 하게 된다.class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
var reference1: Person?
var reference2: Person?
var reference3: Person?
이렇게 클래스가 있는데 아래에서 optional로 선언했다. 그렇기 때문에, 아직 인스턴스화 되지 않은 상태이다.
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
인스턴스화를 하게 되면 생성자가 호출되면서 출력을 하게 된다. 이렇게 인스턴스화 하게된 이후에는 ARC는 해당 인스턴스의 참조를 추적하게 된다. 지금은 reference1
이라는 변수가 참조하고 있기 때문에 reference count는 1이고, 1이기 때문에 할당해제 되지 않는다.
reference2 = reference1
reference3 = reference1
그러면 이렇게 두개의 추가 변수가 같은 인스턴스를 가리킨다면 어떨까? reference count가 3이된다.
reference1 = nil
reference2 = nil
이렇게 두개를 참조 해제하게 되면 reference count는 1이 되겠다.
reference3 = nil
// Prints "John Appleseed is being deinitialized"
이렇게 nil로 변경하게 되면, 최종적으로 reference count는 0이되고, 할당해제 된다.
strong reference cycle
이라 한다.weak
또는 unowned
reference를 사용해서 이런 상황을 해결할 수 있다.class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
john = nil
unit4A = nil
스위프트는 두가지 방법을 제공한다. weak
, unowned
이 두가지이다.
nil
을 줘버린다.weak
을 사용할 경우 무조건 var 로 선언해야 한다.optional
이 되기 때문에 무조건 optional
로 선언해야 한다. class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
아까와 달리 Apartment에서 tenant가 weak으로 선언되어 있다. 그리고 나서 위와 같이 선언을 해준 상태이다. 이제 이상황에서 john의 참조를 끊어보자.
john = nil
// Prints "John Appleseed is being deinitialized"
tenant는 weak 참조이기 때문에 Person instance의 reference count는 0이 되어 할당이 해제 된다. ARC는 weak으로 참조하고 있던 tenant 변수의 값을 nil로 변경한다.
이제 이 상황에서 unit4A도 할당을 해제해보자.
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
원하는 결과를 얻었다! Apartment instance도 reference count가 1이었기 때문에, unit4A가 참조를 해제하는 순간 0이되어 할당이 해제되었다.
unowned
역시 강한 참조를 하지는 않는다.weak
참조와 다르게, 이녀석은 항상 값을 가지고 있기를 기대한다.unowned
로 선언하게 되면, ARC는 value를 nil로 변경하지 않는다. unowned
로 선언된 값이 인스턴스가 해제된 이후 접근하게 되면 런타임 에러가 난다.class Customer {
let name: String
var card: CreditCard?
init(name: String){
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
자 이제, 실제로 돌아가는 건 그림을 보면서 이해하자.
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
그림으로 보면 이러한 상황이다. 이 상태에서 john의 참조를 해재해 보자.
그러면 이러한 상태가 될 텐데, unowned로 참조하고 있기 때문에, 고객 인스턴스는 할당 해제된다. 여기서, 그러면 신용카드 인스턴스도 강한 참조가 없기 때문에 할당 해제된다.
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
구분 | GC | RC |
---|---|---|
참조 계산 시점 | Run Time 어플 실행 동안 주기적으로 참조를 추적하여 사용하지 않는 instance를 해제함 | Compile Time 컴파일 시점에 언제 참조되고 해제되는지 결정되어 런타임 때 그대로 실행됨 |
장점 | 인스턴스가 해제될 확률이 높음 (RC에 비해) | 개발자가 참조 해제 시점을 파악할 수 있음 RunTime 시점에 추가 리소스가 발생하지 않음 |
단점 | 개발자가 참조 해제 시점을 파악할 수 없음 RunTime 시점에 계속 추적하는 추가 리소스가 필요하여 성능저하 발생될 수 있음 | 순환 참조가 발생 시 영구적으로 메모리가 해제되지 않을 수 있음 |