먐
[Swift] 엄청난 ARC 본문
Swift는 앱의 메모리를 관리하기 위해 ARC를 사용한다.
ARC는 인스턴스가 더이상 필요하지 않을 때 자동으로 인스턴스에 의해 사용된 메모리를 할당 해제한다.
→ 프로그래머들이 그동안 메모리를 직접적으로 관리하는 코드를 (항상 반드시 맨날맨날 필수적으로) 쓰지 않았던 이유!
하지만 예외의 상황도 존재하고 이를 위해 우리는 ARC에 대해 알 필요가 있다.
ARC(Automatic Reference Counting)
약자보다 풀네임이 이해하기 쉽다. reference count를 자동으로 관리하는 것.
이름에 맞게 reference type, 즉 클래스에만 적용된다.
구조체와 열거형은 value type이므로 reference로 저장되거나 전달되지 않는다.
ARC의 작동 원리
- 클래스의 인스턴스가 생성되면 ARC는 메모리의 청크에 할당
- 청크: 동적으로 메모리를 할당할 때 사용되는 일정한 크기의 메모리 블록
- 해당 인스턴스와 관련된 저장 프로퍼티의 값, 인스턴스의 타입에 대한 정보를 포함한다.
- 인스턴스가 더이상 필요치 않을 때는 인스턴스에 의해 사용된 메모리를 할당 해제
인스턴스가 더이상 필요하지 않을 때만 할당을 해제해야 하므로
ARC는 프로퍼티, 상수, 변수가 클래스 인스턴스를 참조하고 있는지에 대한 참조 카운트를 추적한다.
→ 이것을 위해 프로퍼티, 상수, 변수에 클래스 인스턴스를 할당할 때마다 해당 프로퍼티, 상수, 변수는 인스턴스에 강한 참조(strong reference)를 만든다.
따라서 우리는 강한 참조 카운트에 집중해야 한다.
- 강한 참조가 남아 있는 한 할당 해제를 하지 않기 때문에, 강한!!!!!! 참조라고 한다.
- 참조가 하나라도 존재한다면 할당 해제하지 않는다.
ARC의 작동 원리를 보여주는 간단한 예제
class Person {
let name: String
init(name: String) {
self.name = name
print("initialized...")
}
deinit {
print("deinitialized...")
}
}
초기화 시 initialized... 출력하고 할당 해제 시 deinitialized... 출력하는 클래스 구현했다.
var ref1: Person?
var ref2: Person?
var ref3: Person?
Person 타입의 변수 3개 선언
현재는 nil 값으로 초기화된다.
ref1 = Person(name: "Mini")
// initialized...
ref1 변수에 Person 인스턴스를 생성하여 할당해 보자.
initialized가 출력된다.
ref1 에서 Person 인스턴스에 대해 강한 참조가 존재하므로, 이 Person 을 메모리에서 유지하고 할당 해제하지 않는다.
ref2 = ref1
ref3 = ref1
동일한 Person 인스턴스를 2개 이상의 변수에 할당해 보자.
현재 Person 인스턴스에 대해 3개의 강한 참조가 설정되어 있다.
ref1 = nil
ref2 = nil
ref1 , ref2 에 nil을 할당하여 강한 참조를 중단하면, 현재 Person 인스턴스에는 한 개의 강한 참조가 남게 된다.
여전히 강한 참조가 존재하므로, Person 인스턴스가 할당 해제되지는 않는다.
ref3 = nil
// deinitialized...
마지막으로 모든 강한 참조가 중단되면, Person 인스턴스는 할당 해제된다.
어디에서도 Person 인스턴스를 더이상 사용하지 않는 것이 명백하기 때문이다.
순환 참조
앞서 설명했듯이 ARC는 참조 카운트를 추적하여 메모리에서 인스턴스를 할당 해제한다.
하지만 만약 우리가 참조 카운트가 0이 될 수 없는 코드를 작성했다면, 인스턴스는 ARC에 할당 해제가 되지 않고, 이는 메모리 누수를 유발할 수 있다.
이것을 강한 참조 사이클(strong reference cycle)이라고 한다. (순환 참조라고도 함.)
이러한 경우는 바로 두 클래스 인스턴스가 서로에 대한 강한 참조를 유지하는 경우이다.
예제
이를 이해할 수 있는 간단한 예제를 만들어 보자.
class Person: {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class Apartment {
let unit: String // 집 하나의 이름
var tenant: Person? // 세입자
init(unit: String) {
self.unit = unit
}
deinit {
print("Apartment \(unit) is being deinitialized")
}
}
2개의 클래스를 만들었다.
[Person]
name , apartment 프로퍼티를 가지고 있다. 사람이 항상 아파트를 사는 것은 아니므로 옵셔널로 선언
[Apartment]
unit, tenant 프로퍼티를 가지고 있다.
항상 세입자가 있는 것은 아니므로 옵셔널로 선언했다.
이 두 클래스를 사용하여 아파트와 거주자를 표현하는 코드를 구현해 보자.
var mini: Person?
var unitA: Apartment?
mini = Person(name: "Mini")
unitA = Apartment(unit: "1A")
mini , unitA 변수에 인스턴스를 생성하여 할당해 보자.
mini!.apartment = unitA
unitA!.tenant = mini
mini는 아파트를 가지게 되었고, unitA 집은 세입자 mini 를 얻은 상황을 코드로 표현하면 위와 같다.
이때 두 인스턴스는 서로 간의 강한 참조 사이클이 생성된다.
mini = nil
unitA = nil
mini 와 unitA 의 강한 참조를 중단하여도, 참조 카운트가 0이 되지 않아 인스턴스는 할당 해제될 수 없다.
당연히 초기화 해제 구문도 실행되지 않는다.
순환 참조을 해결하려면?
위에서 설명한 강한 참조 사이클을 해결하기 위해서는
약한 참조(weak reference), 미소유 참조(unowned reference) 2가지 방법이 존재한다.
둘다 강한 참조를 사용하지 않는 방법으로 다른 인스턴스를 참조한다.
약한 참조(weak reference)
강하게 참조하지 않아서 강한 참조 사이클이 발생하지 않는다.
따라서 약한 참조로 참조할 때는 인스턴스가 할당 해제될 수 있다.
참조하던 인스턴스가 할당 해제되면 자동으로 nil 로 설정한다.
이를 위해서는 항상 옵셔널 타입의 변수(var)로 선언되어야 한다. (nil로 변경되어야 하니까…!)
프로퍼티나 변수 선언 전에 weak 키워드 작성하여 weak 참조를 할 수 있다.
보통 참조하는 인스턴스의 수명이 더 짧을 경우 weak로 참조한다.
앞서 작성했던 예제의 강한 참조 사이클을 weak 참조로 참조하여 변경해 보자
class Person {
let name: String
var apartment: Apartment?
init(name: String) {
self.name = name
}
deinit {
print("\(name) is being deinitialized")
}
}
class Apartment {
let unit: String // 집 하나의 이름
weak var tenant: Person? // 세입자
init(unit: String) {
self.unit = unit
}
deinit {
print("Apartment \(unit) is being deinitialized")
}
}
var mini: Person?
var unitA: Apartment?
mini = Person(name: "Mini")
unitA = Apartment(unit: "1A")
mini!.apartment = unitA
unitA!.tenant = mini
weak var tenant: Person? 로 선언했기 때문에
Apartment 인스턴스는 Person 인스턴스에 대해 약한 참조를 가지게 된다.
mini = nil // Mini is being deinitialized
이때 mini 에 nil을 할당하여 강한 참조를 끊으면 Person 인스턴스에 대해 더이상 강한 참조가 존재하지 않는다.
강한 참조 카운트가 0이므로 Person 인스턴스는 할당 해제되고, 초기화 해제 구문이 출력된다.
또한 Person 인스턴스에 대해 약한 참조를 가지던 tenant 변수는 nil로 설정된다.
unitA = nil // Apartment 1A is being deinitialized
unitA 에 ni을 할당하면 Apartment 인스턴스에 대한 강한 참조의 카운트는 0이므로 마찬가지로 할당 해제 된다.
정리하자면, weak로 참조하면 reference count가 증가하지 않고,
참조하던 인스턴스가 할당 해제되면 nil이 자동으로 할당된다.
미소유 참조(unowned reference)
약한 참조처럼 인스턴스를 강하게 참조하지 않아 reference count가 증가하지 않는다.
프로퍼티나 변수 선언 전에 unowned 키워드를 작성하여 참조한다.
unowned 참조는 항상 값이 존재할 것이라고 가정하는데, 이게 무슨 뜻일까????!!!!
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\\(name) deinitialized...")}
}
class CreditCard {
let number: UInt64
unowned var customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card $\\(number) deinitialized...")}
}
[Customer] 고객의 이름, 카드 멤버 프로퍼티
[CreditCard] 카드 번호, 카드를 소유한 고객 멤버 프로퍼티
고객은 신용카드를 가지고 있을 수도, 가지고 있지 않을 수도 있다.
반면에 신용카드는 항상 고객에 의해 소유되어야 한다.
따라서 CreditCard의 customer 프로퍼티의 값은 항상 존재할 것이고,
CreditCard 의 인스턴스는 참조하는 Customer 인스턴스보다 오래 지속되지 못한다.
→ 참조하는 인스턴스의 수명이 같거나 더 긴 경우에는 unowned 로 참조한다.
var mini: Customer?
var card: CreditCard?
mini = Customer(name: "Mini")
card = CreditCard(number: 1234_5678_9012_3456, customer: mini!)
mini!.card = card
Customer 인스턴스의 card 프로퍼티가
Customer 인스턴스를 unowned로 참조하고 있는 CreditCard 인스턴스를 참조하도록 하자.
CreditCard 인스턴스는 Customer 인스턴스를 unowned 로 참조하고 있으므로,
현재 Customer 인스턴스를 강하게 참조하고 있는 것은 mini 뿐이다.
mini = nil
// Mini deinitialized...
따라서 mini 에 nil을 할당하면, Customer 인스턴스는 할당 해제된다.
unowned 의 reference count가 증가하지 않음을 나타내기 위해 mini 에 nil을 할당하긴 했지만,
customer 인스턴스가 먼저 할당 해제 됐을 경우에
card?.customer
Error Message
Fatal error: Attempted to read an unowned reference but object 0x600000c03060 was already deallocated
customer 프로퍼티에 접근하려 하면 이미 메모리에서 해제되었다는 에러가 발생한다.
따라서 참조하는 인스턴스의 수명 주기가 같거나 더 긴 경우에만 unowned로 참조해야 한다.
weak 참조와 다르게 자동으로 nil로 할당되든가! 하지 않기 때문이다.
클로저에 대한 순환 참조
순환 참조는 인스턴스의 프로퍼티에 클로저를 할당하고,
해당 클로저 내에서 인스턴스를 캡처하는 경우에도 발생할 수 있다.
예를 들면 self.someProperty 처럼 클로저가 self 를 캡처하는 경우이다.
캡처에 대해서 잘 기억이 안 난다면,,,
[Swift] 캡처, 캡처리스트
캡처 클로저는 정의된 둘러싸인 컨텍스트에서 상수와 변수를 캡처 (capture)할 수 있습니다. 그러면 클로저는 상수와 변수를 정의한 원래 범위가 더이상 존재하지 않더라도 본문 내에서 해당 상수
don2bye.tistory.com
클로저도 클래스와 같이 reference type이기 때문에 순환 참조가 발생한다.
프로퍼티에 클로저를 할당하면 해당 클로저의 참조를 할당하게 되는 것이다.
예제를 통해 살펴 보자.
class SampleClass {
var name: String
lazy var someProperty: () -> Void = {
print("\(self.name)에서 클로저가 호출되었습니다. 😇")
}
init(name: String) {
self.name = name
}
deinit { print("\(name) deinitalized...") }
}
[name] 클래스의 이름을 저장할 프로퍼티
[someProperty] 클로저에 대한 참조가 할당되는 프로퍼티
참고) 초기화 전의 name을 사용하기 위해서는 lazy 변수로 선언해야 한다.
var sampleClass: SampleClass?
sampleClass = SampleClass(name: "sampleClass")
sampleClass 변수를 선언하고 새로운 인스턴스를 생성해 보자.
sampleClass 변수는 SampleClass 인스턴스를 강하게 참조한다.
sampleClass?.someProperty()
// sampleClass에서 클로저가 호출되었습니다. 😇
다음과 같이 someProperty 를 호출하면 본문에 작성했던 구문이 출력된다.
현재 참조 상황은 위와 같다.
sampleClass 는 SampleClass 인스턴스를 강하게 참조하고 있고,
SampleClass 인스턴스의 someProperty 는 클로저의 참조를 할당하여 클로저를 강하게 참조하고 있다.
마찬가지로 클로저에는 self.name을 통해 self를 강하게 참조하고 있다.
참고로 클로저가 여러번 self 를 참조해도, 클로저는 인스턴스에 대해 하나의 강한 참조만 캡처한다.
따라서 sampleClass 에 nil을 할당하여 강한 참조를 중단해도 deinit 구문은 출력되지 않는다.
아직 클로저가 SampleClass 인스턴스를 참조하고 있기 때문이다.
이때 순환 참조가 발생한다.
클로저에 대한 순환 참조를 해결하려면!
우리는 캡처리스트를 통해서 값이 캡처되는 방식을 제어할 수 있다고 배웠는데,
이를 활용하여 클로저의 순환 참조도 해결할 수 있다.
두 클래스간 순환 참조를 해결할 때처럼 캡처되는 참조를 weak, unowned로 선언하면 된다.
둘다 강한 참조를 하지 않으므로 reference count가 증가하지 않는다.
- 인스턴스를 weak 로 참조하기
lazy var someProperty: () -> Void = { [weak self] in
guard let self = self else { return }
print("\(self.name)에서 클로저가 호출되었습니다. 😇")
}
클래스 간 순환 참조 발생 시에 참조하던 인스턴스가 할당 해제됐을 때 nil 을 자동 할당해 주는 것처럼,
캡처한 참조가 후에 nil 이 될 가능성이 있다면 weak 로 참조해야 한다.
이때 nil이 될 가능성이 있으므로 옵셔널 언래핑이 필요하다.
sampleClass?.someProperty() // sampleClass에서 클로저가 호출되었습니다. 😇
sampleClass = nil // sampleClass deinitalized...
sampleClass 의 인스턴스에 대한 강한 참조를 중단하면, reference count가 0이 되어 클래스 인스턴스가 할당 해제된다.
클래스 인스턴스가 할당 해제되면 인스턴스의 멤버들도 해제되니 클로저도 할당 해제된다.
2. 인스턴스를 unowned로 참조하기
lazy var someProperty: () -> Void = { [unowned self] in
print("\(self.name)에서 클로저가 호출되었습니다. 😇")
}
클래스 인스턴스간 순환 참조 해결 때와 마찬가지로
unowned 로 참조하는 인스턴스는 할당 해제되어도 nil로 바뀌지 않는다.
따라서 weak 로 참조할 때와 달리 옵셔널 언래핑이 필요없다.
sampleClass?.someProperty() // sampleClass에서 클로저가 호출되었습니다. 😇
sampleClass = nil // sampleClass deinitalized...
마찬가지로 sampleClass 에 nil을 할당하면,
더이상 SampleClass 인스턴스를 참조하는 객체가 없으므로 할당 해제된다.
unowned는 대체 언제 사용하는 걸까…?
안정성이 더 높은 weak 로 참조하는 것이 맞다고 생각했지만,
unowned로 참조하면 옵셔널 언래핑이 필요 없어 코드가 직관적이고 간결해진다.
또한 해당 클로저가 실행되는 동안 self는 반드시 존재할 것이라는 의도를 명확하게 나타낼 수 있다.
사용할 수 있는 경우엔 사용해 보자… 많은 공부가 될 것 같다. 😇
Reference
https://bbiguduk.gitbook.io/swift/language-guide-1/automatic-reference-counting
'iOS > 문법' 카테고리의 다른 글
[Swift] init (0) | 2024.04.02 |
---|---|
[Swift] inout 파라미터의 개념과 동작 과정 (1) | 2024.04.01 |
[Swift] Property Wrapper (0) | 2024.04.01 |
[Swift] 캡처, 캡처리스트 (0) | 2024.04.01 |
[Swift] Swift의 클로저 (0) | 2024.04.01 |