클래스 인스턴스간 강한 참조 순환
(Strong Reference Cycles Between Class Instances)
클래스의 인스턴스 간 강하게 상호 참조를 하고 있는 경우에는 ARC에서 자동으로 메모리 해제를 못하는 경우도 있다.
Person 클래스와 Apartment 클래스가 있다.
Person 클래스 내의 apartment 저장 프로퍼티 경우는 Apartment 클래스 타입을 따르고 있고,
Apartment 클래스 내의 tenant 저장 프로퍼티 경우는 Person 클래스 타입을 따르고 있다.
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") }
}
Person과 Apartment 타입에 따르는 변수를 선언한다.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
현재 변수와 인스턴스 상태를 그림으로 보면 아래와 같다.
이 상태에서 john의 apartment와 unit4A의 tenant에 값을 할당한다.
john!.apartment = unit4A
unit4A!.tenant = john
인스턴스 안의 apartment, tenant가 각각 Apartment, Person 인스턴스를 참조하고 있는 상황이 되고,
이에 Apartment, Person 인스턴스의 참조 횟수는 각각 2가 된다.
여기서 john과 unit4A에 nil을 할당하여 인스턴스를 해제하려고 하지만 해제가 되지 않는다.
john = nil
unit4A = nil
john과 unit4A는 각 인스턴스에 대한 참조를 하고 있지 않지만, Person 인스턴스와 Apartment 인스턴스의 변수가 각각 상호 참조하고 있어서 (참조 횟수 1) 두 인스턴스는 해제되지 않고 메모리 누수가 발생한다.
클래스 인스턴스간 강한 참조 순환 문제의 해결
(Resolving Strong Reference Cycles Between Class Instances)
강한 순환 참조 문제를 해결 하기 위해서는 weak(약한 참조)와 unowned(미소유 참조)를 사용한다.
두 참조 모두 ARC에서 참조 횟수를 증가시키지 않고 인스턴스를 참조한다.
약한 참조 (weak reference)
약한 참조로 선언하면 참조하고 있는 것이 먼저 메모리에서 해제되기 때문에
ARC는 약한 참조로 선언된 참조 대상이 해제되면 런타임에 자동으로 참조하고 있는 변수에 nil을 할당한다.
Apartment의 tenant 변수는 weak로 선언했다.
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? // weak
deinit { print("Apartment \(unit) is being deinitialized") }
}
Person 인스턴스와 Apartment 인스턴스의 변수에서 각각 인스턴스를 상호 참조하도록 할당한다.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
Apartment의 tenant변수가 Person 인스턴스를 약한 참조(weak)로 참조하고 있다.
이 시점에서 Person 인스턴스에 대한 참조 횟수는 john이 참조하고 있는 1이다.
john의 참조 대상을 nil로 할당하면 더 이상 Person 인스턴스를 참조하는 것이 없게 된다.
john = nil
// Prints "John Appleseed is being deinitialized"
unit4A에 nil을 할당하면 Apartment 인스턴스를 참조하는 개체도 사라지게 되고,
Apartment 인스턴스는 메모리에서 해제된다.
미소유 참조(Unowned References)
미소유 참조는 약한 참조와 다르게 참조 대상이 되는 인스턴스가 현재 참조하고 있는 것과 같은 생애주기 또는 더 긴 생애주기를 갖기 때문에 항상 참조에 그 값이 있다고 기대한다.
따라서 ARC는 미소유 참조에는 절대 nil을 할당하지 않는다.
미소유 참조는 옵셔널 타입을 사용하지 않고, 만약 미소유 참조로 선언된 인스턴스가 해제되었는데 접근하게 되면 런타임 에러가 발생한다.
Customer는 card 저장 프로퍼티로 CreditCard 인스턴스를 참조하고 있고 CreditCard는 customer으로 Customer 인스턴스를 참조하고 있다. 이 때 customer는 미소유 참조 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 // unowned
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
고객 변수 john을 옵셔널 타입으로 선언 후 인스턴스를 생성하고 고객의 카드 변수에도 카드 인스턴스를 할당한다.
var john: Customer?
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
John이 Customer 인스턴스를 참조하고 있고 CreditCard 인스턴스도 Customer Instance를 참조하고 있지만 미소유(unowned) 참조를 하고 있기 때문에 Customer 인스턴스에 대한 참조 횟수는 1회가 된다.
여기서 john 변수의 Customer 인스턴스 참조를 끊게 되면 다음과 같아진다.
더 이상 Customer 인스턴스를 강하게 참조하고 있는 인스턴스가 없으므로 Customer 인스턴스가 해제되고 인스턴스가 해제됨에 따라 CreditCard 인스턴스를 참조하고 있는 개체도 사라지므로 CreditCard 인스턴스도 메모리에서 해제된다.
미소유 참조와 암시적 옵셔널 프로퍼티 추출
(Unowned References and Implicitly Unwrapped Optional Properties)
약한 참조와 미소유 참조의 구분은 해당 참조가 nil이 될 수 있는지 여부로 판단한다.
이 외에도 두 프로퍼티가 항상 값을 갖지만 한번 초기화 되면 절대 nil이 되지 않는 경우가 있다.
이 때에는 미소유 프로퍼티를 암시적 옵셔널 프로퍼티 추출을 사용해 해결 할 수 있다.
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
Country의 capitalCity는 초기화 단계에서 City 클래스에 초기화된 후 사용되게 된다. 실제로 Country의 capitalCity는 옵셔널이 돼야 맞지만, 여기서 느낌표 ! 를 이용하여 명시적으로 강제 언래핑을 시켰다. 암시적 언래핑이 되어 Country에서 name이 초기화되는 시점에 self를 사용할 수 있게 된다. 그리고 City에서는 강한 참조 순환을 피하기 위해 미소유 참조로 country를 선언해서 두 인스턴스를 문제없이 사용한다.
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"
'Swift > 기본' 카테고리의 다른 글
ARC 3편 (클로저에서의 강한 참조 순환) (0) | 2024.04.26 |
---|---|
DispatchQueue.main과 DispatchQueue.global (1) | 2024.04.20 |
ARC 1편 - ARC 란? (0) | 2024.04.19 |
메모리 구조 (Code, Data, Stack, Heap) (1) | 2024.04.18 |
접근 제어자 (Access Control) (0) | 2024.04.17 |