반응형
Overview
- Swift는어플리케이션의 메모리 사용을 추적하고 관리하기 위해 Automatic Reference Counting (ARC) 를 사용한다.
- 대부분의 경우에서 ARC는 Swift에서 메모리 관리가 "Just Works" 한다는 의미이다.
- 따라서 개발자는 스스로 메모리 관리에 대해서 신경쓸 필요가 없다.
- ARC 는 클래스 인스턴스가 사용했던 메모리를 자동으로 해제시켜주며 이 과정은 해당 인스턴스가 불필요해지는 시점에서 이뤄진다.
- 하지만 일부 경우에서 ARC가 메모리를 관리하기위해 코드 사이의 관계에 대한 더 많은 정보를 더 요구하는 경우가 있다.
- 이 chapter 에서는 위과 같은 상황을 묘사하며 어떻게 ARC가 어플리케이션의 메모리를 관리하고 있는지 설명해준다.
- Swift 에서 ARC 를 사용하는 것은 Transitioning to ARC Release Notes 에 명시된 Objective-C를 통한 ARC 과 유사한 접근 방식을 사용하고 있다.
- Reference Counting 개념은 오직 클래스의 인스턴스에만 적용된다.
- 구조체나 열거형은 reference 타입이 아닌 value 타입이기 때문에 reference 정보를 저장하거나 넘겨 주지 않는다.
How ARC Works
- 개발자가 새로운 클래스의 인스턴스를 생성하는 매 순간, ARC 는 메모리 조각(chunk) 를 할당하여 인스턴스에 대한 정보를 저장할 수 있도록 한다.
- 이 메모리는 인스턴스의 타입에 대한 정보를 갖고 있으며, 해당 인스턴스와 연관된 어느 저장 프로퍼티의 값도 함께 갖고 있다.
- 추가적으로, 어느 인스턴스가 더이상 필요하지 않을 때 ARC 는 인스턴스가 사용하던 메모리를 해제시켜준다.
- 메모리 해제 과정을 통해 해당 인스턴스가 사용하던 메모리 공간을 다른 목적으로 사용할 수 있게 된다.
- 다시 말해 필요하지 않은 클래스의 인스턴스가 더이상 메모리 공간을 차지하지 않는다는 점을 보장해준다.
- 하지만 만약 ARC가 현재 사용중인 인스턴스 메모리를 deallocate 시킬 경우 메모리 해제된 인스턴스의 프로퍼티에 더이상 접근할 수 없으며 인스턴스의 메서드도 호출할 수 없게 된다.
- 실제로 메모리 해제된 인스턴스에 접근하려고 시도한다면 앱에서 충돌(crash)이 발생하게 된다.
- 인스턴스가 여전히 필요한 동안 메모리 상에서 사라지지 않도록 보장하는 방법이 ARC에 존재한다.
- ARC는 해당 클래스의 인스턴스를 현재 참조하고 있는 프로퍼티, 상수, 변수가 얼마나 존재하는지 추적한다.
- ARC는 해당 인스턴스를 참조하고 있는 적어도 하나의 활성화된 reference가 존재하는 동안에는 인스턴스를 deallocate 시키지 않는다.
- 이런 과정을 가능하게 만드려면, 개발자가 클래스의 인스턴스를 프로퍼티, 상수나 변수에 할당할 때마다 해당 프로퍼티, 상수나 변수가 인스턴스와 strong reference 를 갖도록 구성해야 한다.
- Strong Reference 로 부르는 이유는 인스턴스와 강하고 확실한 연결을 유지하고 있기 때문이며 Strong Reference 가 남아있는 동안에는 메모리가 해제되는 것을 허용하지 않기 때문이다.
ARC in Action
- 다음의 예시는 어떻게 Automatic Reference Counting 이 동작하는지에 관한 예시이다.
- 이 예시는 Person 으로 불리는 단순한 클래스에서 시작되며 name 으로 불리는 상수 저장 프로퍼티가 선언되었다.
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initiailized")
}
deinit {
print("\(name) is being deinitialized")
}
}
- Person 클래스는 생성자를 갖고 있으며 인스턴스의 name 프로퍼티를 설정하고 생성과정이 이뤄졌음을 의미하는 메시지를 출력한다.
- Person 클래스는 파괴자를 갖고 있으며 클래스의 인스턴스가 메모리에서 해제될 때 메시지를 출력한다.
- 다음의 코드 조각은 optional Person 타입의 3가지 변수를 선언한 것이다.
- 새로운 Person 인스턴스에 대하여 뒤따르는 코드 조각에서 여러개의 reference 를 설정하는데 사용된다.
- 왜냐하면 3가지의 변수들은 옵셔널 타입이기 때문에 자동으로 nil 값을 갖도록 생성되며 현재까지는 Person의 인스턴스를 reference 하지 않는 상태이기 때문이다.
var reference1: Person?
var reference2: Person?
var reference3: Person?
- 개발자는 이제부터 새로운 Person 인스턴스를 생성할 수 있으며 3가지 변수중 하나에 할당시킬 수 있다.
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
- 메시지 "John Appleseed is being initialized" 가 Person 클래스의 생성자를 호출한 시점에서 출력된다는 점에 주목해야한다.
- 이 현상은 생성자가 호출되었음을 입증해준다.
- 왜냐하면 새로운 Person 인스턴스가 reference1 변수에 할당되었으며 reference1으로부터 새로운 Person 인스턴스에 강한 참조가 이뤄졌기 때문이다.
- 왜냐하면 적어도 하나의 강한 참조가 존재할 때, ARC 는 해당 Person 을 메모리 상에 유지시켜서 deallocate 되지 않도록 만들어주기 때문이다.
- 만약 같은 Person 인스턴스를 2개 이상의 변수에 할당하는 경우에 해당 인스턴스에 대한 2개 이상의 강한 참조가 구성된다.
reference2 = reference1
reference3 = reference1
- 위와 같이 코드를 작성하면 현재 하나의 Person 인스턴스에 3개의 강한 참조가 존재한다.
- 만약 해당 변수에 nil 값을 할당함으로써 원본 레퍼런스를 포함해서 2개의 강한 참조를 끊어준다면 Person 인스턴스에는 하나의 강한 참조만 남아있으며 따라서 Person 인스턴스는 deallocate 되지 않는다.
reference1 = nil
reference2 = nil
- ARC는 3번째이자 마지막 강한 참조가 끊어지지 않는다면 Person 인스턴스를 deallocate 하지 않는다.
- 마지막 강한 창조가 끊어지는 시점은 더이상 Person 인스턴스가 사용되지 않음이 명확한 시점일 것이다.
reference3 = nil
// Prints "Jhon Appleseed is being deinitialized"
Strong Reference Cycles Between Class Instances
- 다음의 예시에서 ARC 는 개발자가 생성한 새로운 Person 인스턴스에 참조 횟수를 추적할 수 있으며 해당 인스턴스가 더이상 필요하지 않는 경우 deallocate 할 수 있다.
- 하지만 클래스의 인스턴스가 0개의 강한참조를 갖는 지점에 도달하지 않는 코드를 작성하는 것이 가능하다.
- 만약 두개의 클래스 인스턴스가 서로에 대해 강한 참조를 갖고 있을 때 위와 같은 상황에서 발생할 수 있다.
- 각 인스턴스가 서로를 메모리 상에 살아있는 상태로 유지시켜주는 것이다.
- 위와 같은 문제 상황은 strong reference cycle 로 알려져있다.
- strong reference cycle 상황을 해결하기위해 다음과 같은 방법을 사용할 수 있다.
- 클래스들 간에 일부 관계를 선언할 때 strong 참조 대신에 weak 혹은 unowned 참조를 활용하는 방법이다.
- 이와 관련한 절차가 다음에 나오는 Resolving Strong Reference Cycles Between Class Instances 에 묘사되어있다.
- 그러나 강한 참조 사이클 문제를 해결하는 방법을 배우기 전에 어떤 원인으로 이와 같은 사이클이 발생하는지 이해하는 것이 유용할 수 있다.
- 다음의 예시는 어떻게 강한 참조 사이클이 우연히 발생될 수 있는지 설명해준다.
- 이 예시에서는 Person과 Apartment하는 두개의 클래스들을 선언하고 있으며 이들은 아파트 블럭과 여기에 사는 거주민에 대한 모델이다.
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 인스턴스는 String 타입의 name 프로퍼티를 갖고 있으며 옵셔널 apartment 프로퍼티는 초기에 nil 값으로 설정되어있다.
- apartment 프로퍼티는 옵셔널인데 왜냐하면 한 사람이 언제나 apartment를 갖고 있지 않을 수 있기 때문이다.
- 비슷하게 모든 Apartment 인스턴스는 String 타입의 unit 프로퍼티를 갖고 있으며 옵셔널 tenant 프로퍼티는 초기에 nil 값으로 설정되어있다.
- 이 tenant 프로퍼티는 옵셔널인데 왜냐하면 어느 아파트는 소유자가 존재하지 않을 수 있는 경우가 있기 때문이다.
- 두 클래스에는 deinitializer 가 선언되어있는데, 해당 클래스의 인스턴스가 파괴되었다는 사실을 출력해준다.
- 이 현상은 개발자가 Person 과 Apartment 의 인스턴스가 예상대로 deallocate 되었음을 확인할 수 있도록 도와준다.
- 다음dml 코드 조각은 john과 unit4A 로 불리는 옵셔널 타입의 두 변수가 선언되어있으며 아래에서 구체적인 Apartment 와 Person 인스턴스가 설정될 것이다.
- 두개의 변수 모두 옵셔널이기 때문에 초기값으로 nil을 갖고 있다.
var john = Person?
var unit4A: Apartment?
- 개발자는 구체적인 Person 과 Apartment 인스턴스를 생성할 수 있으며 이와 같은 새로운 인스턴스를 john 과 unit4A 변수에 할당할 수 있다.
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
- 다음은 두개의 인스턴스를 생성하고 할당한 후에 어떻게 강한 참조를 봐야 하는지 설명해준다.
- john 변수는 새로운 Person 인스턴스에 대한 강한 참조를 갖고 있다.
- unit4A 변수는 새로운 Apartment 인스턴스에 대한 강한 참조를 갖고 있다.
- 개발자는 어느 Person 어느 apartment 를 갖고 apartment에 점유자를 갖도록하기 위해 두 인스턴스를 함께 연결해줄 수 있습니다.
- 여기서 느낌표는 john과 unit4A 옵셔널 변수 내부에 저장된 인스턴스에 unwrap 하여 접근하는 목적으로 사용된다.
- 그래서 해당 인스턴스의 프로퍼티가 다음과 같이 설정될 수 있다.
john!.apartment = unit4A
unit4A!.tenant = john
- 다음은 두개의 인스턴스가 함꼐 연결된 이후에 강한 참조가 어떻게 보이는지를 보여주고 있다.
- 불행하게도 두 인스턴스 간에 연결하는 것은 둘 사이에 강한 참조 사이클을 발생시킨다.
- Person 는 이제부터 Apartment 인스턴스에 대한 강한 참조를 갖게 되며, Apartment 인스턴스는 Person 인스턴스에 대한 강한 참조를 갖게 된다.
- 그러므로, john과 unit4A 변수에 의해 잡혀있는 강한 참조를 끊어줄 때, reference count는 0개로 줄어들지 않는다.
- 따라서 인스턴스가 ARC 에 의해서 deallocate 되지 않는다.
john = nil
unit4A = nil
- 주목할 점은 위에 보이는 두 변수가 nil 값으로 설정되어도 deinitializer가 호출되지 않는다는 점이다.
- 강한 참조 사이클이 Person과 Apartment 인스턴스가 deallocate 되지 않도록 막아주고 있기 때문이다.
- 따라서 어플리케이션에서는 메모리 누수(leak)가 발생하게 된다.
- 다음의 예시에서는 john과 unit4A 변수가 nil 값으로 설정된 이후에 당한 참조가 어떻게 보여지는지 보여준다.
- 강한 참조는 Person 인스턴스와 Apartment 인스턴스 사이데 남아있으며 끊어질 수 없다.
Resolving Strong Reference Cycles Between Class Instances
- Swift 는 개발자가 클래스 타입의 프로퍼티에 대하여 작업할 때 발생할 수 있는 강한 참조 사이클 문제를 해결할 수 있도록 두가지 방법을 제공한다
- weak reference 와 unowned reference 이다.
- Weak 와 unowned reference 는 reference cycle 에서 하나의 인스턴스가 다른 인스턴스를 지칭할 때 강하게 붙잡지 않도록 도와준다.
- 따라서 인스턴스는 서로를 강한 참조 사이클을 발생시키지 않고 참조할 수 있게되었다.
- weak reference 는 다른 인스턴스가 짧은 lifetime을 갖고 있을때 사용하며 다른 인스턴스가 먼저 deallocated 될 수 있다는 의미이다.
- 위에서 언급된 Apartment 예시에서 어느 apartment가 lifetime 동안의 어느 지점에서는 tenant 를 갖지 못할 수 있다는 점에서 적합하다.
- 그래서 weak reference 는 위와 같은 상황에서 레퍼런스 사이클을 끊어주는데 적합한 방법이다.
- 반대로 unowned reference는 다른 인스턴스가 동일하거나 더 긴 lifetime 을 갖는 경우에 사용한다.
Weak Reference
- 약한 참조는 refernce 인데 자신이 참조하는 인스턴스를 강하게 붙잡지 않도록 유지한다.
- 그래서 참조된 인스턴스를 놓아주는 것으로 부터 ARC를 멈추지 않는다.
- 이 행위는 reference 가 강한 참조 사이클을 구성하는 부분이 되는 것으로 부터 막아준다.
- 개발자는 weak reference를 프로퍼티나 변수 선언부 앞에 weak 키워드를 붙임으로써 나타낼 수 있다.
- 왜냐하면 weak reference 는 자신이 지칭하는 인스턴스를 강하게 붙잡도록 유지하지 않기 때문에
- weak reference가 여전히 어느 인스턴스를 지칭하는 동안에도 해당 인스턴스를 deallocated 하는 것이 가능하다.
- 따라서 약한 참조가 지칭하던 인스턴스가 deallocated 될 때 weak reference 를 ARC가 자동으로 nil 값으로 설정해준다.
- 왜냐하며 weak reference 들은 runtime에서 그들의 값을 nil 값으로 설정될 수 있어야할 필요가 있기에
- 언제나 상수 대신에 옵셔널 타입의 변수로써 선언된다.
- 개발자는 weak reference 에 값의 존재 여부를 확인할 수 있으며 다른 옵셔널 값을 확인하는 방식과 유사하게 사용할 수 있다.
- 더이상 존재하지 않는 유효하지 않은 인스턴스에 대한 참조가 존재하지 않게 된다.
Note
Property observer 는 ARC가 weak reference 를 nil로 설정할 때 호출되지 않습니다.
- 다음의 예시는 위에선 언급한 Person과 Apartment 예시와 동일하지만 한가지 중요한 차이점이 존재한다.
- 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?
deinit { print("Apartment \(unit) is being deinitialized") }
}
- john과 unit4A와 같은 두 변수의 강한 참조와 두 인스턴스 사이에 연결은 이전처럼 생성된다.
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
- 다음의 예시는 함께 연결된 두 인스턴스의 reference가 어떻게 보여지는지 설명한다.
- Person 인스턴스는 여전히 Apartment 인스턴스에 대해 강한 참조를 갖고 있지만, Apartment 인스턴스는 Person 인스턴스에 대하여 약한 참조를 갖고 있다.
- 이 상황은 만약 john 변수가 갖고 있는 강한 참조를 nil 값으로 설정해서 끊어줬을 때, Person 인스턴스에는 더이상 강한 참조가 존재하지 않는 다는 것을 의미한다.
john = nil
// Prints "John Appleseed is being deinitialized"
- 왜냐하면 Person 인스턴스에 대한 강한 참조가 더이상 존재하지 않기 때문에 해당 인스턴스는 deallocate 되었으며 tenant 프로퍼티는 nil 값으로 설정되었다.
- Apartment 인스턴스에 대하여 오직 하나 남은 강한 참조는 unit4A 변수로 부터 오는 것이다.
- 만약 개발자가 강한 참조를 끊어준다면 더이상 Apartment 인스턴스에 대한 강한 참조가 존재하지 않게 된다.
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
- 왜냐하면 더이상 Apartment 인스턴스에 대한 강한 참조가 존재하지 않기 때문이다.
- 따라서 Apartment 인스턴스도 Person과 마찬가지로 deallocate 된다.
Note
garbage collection 을 사용하는 시스템에서는, 약한 포인터는 때때로 간단한 캐싱 메커니즘을 구현하기 위해 사용된다. 왜냐하면 강한 참조가 존재하지 않는 객체는 오직 memory pressure 가 garbage collection을 실행할 때 deallocated 되기 때문이다. 그러나 ARC 에서는 마지막 강한 참조가 제거되는 순간 value가 deallocated 되어서 그러한 목적으로 weak reference 를 만드는 것은 적절하지 못하다.
Unowned References
- weak reference 와 마찬가지로 unowned reference 는 자신이 지칭하는 인스턴스를 강하게 붙잡도록 유지하지 않습니다.
- 그러나 weak reference 와 다른 점은 unowned reference 는 다른 인스턴스와 동일한 lifetime을 갖거나 더 긴 lifetime 을 가질 때 사용된다는 점이다.
- 개발자는 프로퍼티나 변수의 선언부 이전에 unowned 키워드를 위치시킴으로써 unowed reference 를 나타낼 수 있다.
- weak reference 와 다르게 unowned reference는 언제나 값을 갖도록 기대된다.
- 결론적으로는, unowned 값으로 표현할 때 옵셔널로 만들어주지 않아도 되며 ARC 는 unowned reference’s 값을 nil 로 설정하지 않는다.
Important
unowned reference 는 해당 reference 가 언제나 deallocate 되지 않은 인스턴스를 지칭하는 것이 확실할 때만 사용하세요.
만약 unowned reference 가 지칭하는 인스턴스가 deallocated 된 이후에 접근하려고 시도한다면 runtime error 가 발생하게 됩니다.
- 다음의 예시는 Customer 와 CreditCard 라는 두개의 클래스를 선언하고 있으며 은행의 고객과 그 고객이 사용할 수 있는 신용카드에 대한 모델이다.
- 두 클래스는 다른 클래스의 인스턴스를 프로퍼티로서 각각 저장할 수 있므며 잠재적으로 강한 참조 사이클을 생성할 수 있다.
- Customer 와 CreditCard 사이에 관계는 약한 참조 예시의 Apartment 와 Person 의 관계와 약간 다르다.
- 이 데이터 모델에서 어느 고객은 신용카드를 가질 수도 갖지 않을 수도 있다.
- 반대로 신용카드는 언제나 연관된 고객을 갖고 있을 것이다.
- credit card 인스턴스는 절때 자신이 지칭하는 customer 보다 오래 살아남지 않는다.
- 이런 점을 표현하기 위해서 customer 클래스는 옵셔널 타입의 card 프로퍼티를 갖는다.
- 그러나 CreditCard 클래스는 unowned(그리고 옵셔널이 아닌) customer 프로퍼티를 갖는다.
- 더 나아가, 새로운 CreditCard 인스턴스는 number 값과 customer 인스턴스를 사용자 지정 생성자에 넘겨주어서 생성될 수 있다.
- 이 과정은 어느 CreditCard 인스턴스가 생성될 때 자신과 연관된 customer 인스턴스를 항상 갖고 있다는 것을 보장해준다.
- 왜냐하면 어느 credit card 는 언제나 customer 를 갖고 있으며, 개발자는 CreditCard 의 customer 프로퍼티를 unowned reference 로 만들어줘서 강한 참조 사이클을 피할 수 있다.
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.name = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
Note
CreditCard 클래스의 number 프로퍼티는 Int 타입이 아니라 UInt64 타입으로 선언되었다. 왜냐하면 number 프로퍼티의 가용량을 32비트 시스템과 64비트 시스템에서 16자리 카드 번호를 충분히 저장할 수 있도록 보장하기 위해서이다.
- 다음의 코드 조각은 john 으로 불리는 옵셔널 Customer 변수를 선언하고 있으며 구체적인 customer 의 참조값을 저장하는데 사용될 것이다.
- 이 변수는 옵셔널 타입이기 때문에 초기값으로 nil 을 갖는다.
var john: Customer?
- 당신은 이제부터 Customer 인스턴스를 생성할 수 있으며 이 변수를 새로운 CreditCard 인스턴스를 생성하고 customer의 카드 프로퍼티로 할당하는데 사용할 수 있다.
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
- 다음은 reference 가 어떻게 보이는지 보여주고 있으며 이제부터 두 인스턴스를 연결할 수 있다.
- Customer 인스턴스는 이제부터 CreditCard 인스턴스에 강한 참조를 갖고 있으며 CreditCard 인스턴스는 Customer 인스턴스에 대하여 unowned reference 갖고있다.
- 왜냐하면 unowned customer reference 이기 때문이다.
- 개발자가 john 변수에 의해서 잡혀있는 강한 참조를 끊어줄 때 더이상 Customer 인스턴스에 잡려있는 강한 참조가 존재하지 않게 된다.
- 왜냐하면 Customer 인스턴스에 더이상 강한 참조가 존재하지 않기 때문에 메모리에서 deallocated 되기 때문이다.
- 이 과정이 발생한 후에는 더이상 CreditCard 인스턴스에 대한 강한 참조가 존재하지 않게 되며 마찮가지로 deallocated 된다.
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
- 위에 보이는 마지막 코드 조각은 john 변수가 nil 로 설정된 후에 Customer 인스턴스와 CreditCard 인스턴스의 deinitializer 가 "deinitialized" 메시지를 출력하는 것을 보여준다.
Note
위에서 언급된 예시들은 어떻게 하면 unowned references 를 안전하게 사용할 수 있을지 설명해준다. Swift는 또한 unsafe unowned reference 를 runtime safety check 를 비활성화시킬 필요가 있는 경우를 위해 제공한다. 예를 들어, 성능의 원인이 있을 수 있다. 모든 안전하지 않은 연산과 마찬가지로, 개발자는 해당 코드의 안전성을 확인할 책임이 있다.
개발자는 unsafe unowned reference 를 unowned(unsafe) 로 적어줌으로써 표현할 수 있다. 만약 개발자가 unsafe unowned reference 가 지칭하는 인스턴스가 소멸된 후에 접근하고자 시도한다면, 개발자의 프로그램은 해당 인스턴스가 사용했던 메모리 위치에 접근하려고 시도할 것이며 안전하지 않은 연산이 발생될 것이다.
Strong Reference Cycles for Closures
Resolving Strong Reference Cycles for Closures
반응형
' Apple > Swift Programming Language' 카테고리의 다른 글
[Swift] 공식문서 씹어먹기: Concurrency (0) | 2023.03.18 |
---|---|
[Swift] 공식문서 씹어먹기: Package Manager (0) | 2021.12.20 |
[Swift] 공식문서 씹어먹기: Property - Computed Property (0) | 2021.10.22 |
[Swift] 공식문서 씹어먹기: Collection Type - Dictionary (0) | 2021.10.22 |
[Swift] 공식문서 씹어먹기: Initialization (0) | 2021.10.18 |