문서는 여기 ⬇️
https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html
앞 포스팅에서 Strong Reference Cycle이 만들어져 메모리에서 해제되지 않고 영영 ㅁ ㅣ아가 되어버린 인스턴스가 있을 수 있다는 것을 확인했다. 그렇다면 이런 상황을 방지하려면 어떻게 해야 할까?
Resolving Strong Reference Cycles Between Class Instances
애플에서는 이런 ㅁ ㅣ아들(strong reference cycle)이 생성되는 것을 방지하는 방법을 두 가지 제시한다.
1. weak references
2. unowned references
weak과 unowned를 해석해 보면 '약한'과 '소유자가 없는'이라고 나온다. 두 가지 개념 다 strong과 반대된다. instance를 참조하는 과정에서 강하게 참조하지 않는다는 뜻이 된다.
그렇다면 어떤 상황에서 weak와 unowned를 사용해야 할까? 공식 문서의 글을 번역해서 가져와본다.
다른 인스턴스의 수명이 짧은 경우, 즉 다른 인스턴스의 할당을 먼저 해제할 수 있는 경우 약한 참조를 사용합니다. 위의 아파트 사례에서 아파트는 수명의 어느 시점에 세입자가 없을 수 있는 것이 적절하므로, 이 경우 기준 주기를 깨기 위한 적절한 방법은 약한 참조입니다. 반대로 다른 인스턴스의 수명이 같거나 더 긴 경우 소유하지 않은 참조를 사용합니다.
무슨 말인지.. 대략적으로 알 것 같지만 확실하게 하기 위해 docs의 나머지 부분을 확인해 보자. 마지막에 번역본을 다시 예제와 결합해 설명할 수 있으면 이번 포스팅은 성공!
Weak References (약한 참조)
약한 참조는 프로퍼티 또는 다양한 variable 선언 앞에 weak 키워드를 앞에 붙여 내가 참조할 인스턴스를 약하게 붙잡겠다는 의미를 알린다. 또한 약하게 잡고 있으므로 weak 프로퍼티가 해제되지 않더라도 다른 곳에서 할당이 해제될 수 있다. (다른 곳에서 할당이 해제된다는 건 weak 프로퍼티는 reference count를 증가시키지 않는 말이기도 하다. 또한 nil이 될 수 있다는 의미라 weak 프로퍼티는 언제나 optional로 선언된다.)
그럼 다시 앞 포스팅에서 다뤘던 예제 코드를 가져와보자. Apartment의 tenant 프로퍼티가 weak var 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 }
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
john = nil
unit4A = nil
john과 unit4A에 각각 Person과 Apartment instance가 할당되었고, 각 인스턴스 내부를 보면 Person의 apartment 프로퍼티는 Apartment 인스턴스를 strong 하게 참조하며 Apartment 인스턴스의 tenant 프로퍼티는 Person 인스턴스를 weak으로 참조한다.
이걸 메모리 그래프를 보면!!!
무한의 굴레를 벗어났다!! Person의 apartment는 strong하게 Apartment 인스턴스를 참조하지만 Apartment의 tenant에는 weak으로 할당된 person이 있기 때문에 점선으로 표시가 되었다.
Person 인스턴스를 참조하는 것은 john 뿐이다. (진짜로 Apartment의 tenant는 weak이니까 약해서 참조로도 안쳐줌)
그리고 Apartment 인스턴스를 참조하는 것은 unit4A와 Person의 apartment 두 개다. (Person의 aprtment는 strong)
요런 상황에서 john에 nil을 할당하게 되면?
unit4A가 tenant로 Person 인스턴스를 참조하는 것과 별개로 그냥 Person instance가 메모리에서 해제되어 버린다. 실제로 메모리 그래프에서도 Apartment만 남고 Person은 왼쪽 목록에서 사라졌다.
그렇다는 건 여기 점선으로 연결된 SwiftWeakRefStorage는 아무것도 없다는 얘기.
Person의 deinit 코드가 실행된 것으로도 Person 인스턴스가 해제된 것을 알 수 있다.
그리고 마지막으로 unit4A에 nil을 대입하면 미아가 되는 인스턴스 없이 모두 메모리에서 해제가 된다.
Note
Garbage Collection을 사용하는 시스템에서는 간단한 캐싱 메커니즘을 구현할 때 weak 포인터를 쓰기도 한다(고 한다). Reference Count가 0이 되면 메모리에서 해제하는 ARC와 달리 GC는 실행 중에 사용되지 않는 레퍼런스가 있으면 알아서(동적으로) 메모리에서 해제해 주기 때문에 memory pressure 트리거가 없는 한 레퍼런스가 해제되지 않는다. 따라서 GC에 익숙한 사람들이 ARC 환경에서 weak 키워드를 GC처럼 사용하게 되면 사용하려고 했던 의도와 다르게 동작할 수 있기 때문에 조심해야 한다.
Unowned References (미소유 참조: 네 정보가 필요해서 참조는 할건데 소유는 안 할게~)
weak을 알아봤으니 이번엔 두 번째로 제시했던 Unowned References에 대해 알아보자. unowned도 weak과 마찬가지로 strong 하게 참조하지 않으며 unowned 키워드를 앞에 붙여 미소유 참조를 할 것임을 나타낸다.
weak과 다른 점이라면 unowned를 사용할 때가 지정이 되어있다는 점! 언제 쓰느냐,
unowned로 참조하는 객체의 lifetime이 unowned로 선언된 프로퍼티가 속한 인스턴스의 lifetime보다 같거나 길 때!
그리고 weak과 또 다른 점이 있는데 그것은 바로 unowned 레퍼런스는 항상(always) 값을 가져야 한다는 점이다. 따라서 unowned 키워드가 붙은 프로퍼티는 옵셔널을 사용하지 않아도 되고, ARC 또한 unowned가 붙은 프로퍼티를 nil로 만들지 않는다.
→ unowned reference는 참조하는 인스턴스가 먼저 메모리에서 해제되지 않는다는 것이 보장될 때 사용해야 한다. 만약 unowned로 참조한 인스턴스가 해제되었는데 거기에 대고 인스턴스에 접근을 하면 runtime error 발생!!
Example
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 = nil
앞에서 계속 보던 예제와 다른 예제가 나타났다.
코드를 대충 훑어보면, Customer 클래스는 name과 CreditCard를 갖고, CreditCard 클래스는 number와 Customer를 가진다. (앞에서 보던 예제랑 비슷하게 서로를 참조하게 되는 코드)
john은 Customer 인스턴스를 strong 하게 참조하고, john의 card는 CreditCard 인스턴스를 참조하는데, CreditCard 인스턴스는 customer 프로퍼티가 Customer 인스턴스를 unowned로 참조한다.
말로만 보면 어려우니까 그림으로도 보자.
Customer 인스턴스를 참조하는 친구들에 john과 customer가 보인다. 그런데 customer는 "unowned"라서 Owning references에 0으로 찍힌다! 고로 Customer 인스턴스의 reference count는 1.
이 상황에서 john에 nil을 추가하면?
strong으로 참조하던 john이 사라지면서 Customer의 인스턴스의 reference count가 0이 되고, 동시에 메모리에서 해제가 된다.
CreditCard는 Customer 인스턴스에서만 참조되고 있었는데 Customer 인스턴스가 해제되었으니 당연히 참조하는 곳이 없어서 메모리에서 해제된다.
(이 예제는 두 인스턴스의 lifetime이 동일하다고 말할 수 있지 않을까? 만약에 동일하지 않다면 CreditCard 인스턴스가 먼저 해제되고 Customer가 해제되어야 unowned를 쓸 수 있을 것 같다. 만약 Customer 인스턴스가 해제되고 CreditCard 인스턴스가 남은 상황에서 CreditCard 인스턴스의 customer에 접근하면 크래시가 나니까! 크래시가 나지 않도록 코드의 safety check를 해주어야 한다.)
Unowned Optional References
분명 위에서 unowned reference는 먼저 해제되지 않는 상황에서 쓰니까 값이 있다는 걸 보장하므로 nil이 될 수 없습니다~라고 해놓고 갑자기 optional로 쓸 수 있다고요..? 그럼 도대체 왜 weak 안 쓰고 unowned optional을 쓰는 거죠..?라는 의문이 들었다.
그래.. 침착하게 문서를 읽어보자..
weak과 다른 점이 "The difference is that when you use an unowned optional reference, you’re responsible for making sure it always refers to a valid object or is set to nil."라고 하는데...?
unowned optional reference를 사용할 때는 항상 valid object 또는 nil로 설정해야 할 책임이 있다..... 뭔가 weak과 다르게 사용할 때 의무가 발생한다. valid object를 사용할 것이 아니라면 명시적으로 nil을 써줘야 한다는 것일까? 애플에서 준 예제를 한 번 보자. (ARC Ownership Model)
Department(학과)
학과는 학과명과 강의들을 가짐
departments는 courses를 소유한다. (strong own)
Course(강의)
강의는 강의명과 강의가 개설되는 학과 정보, 다음 코스(심화 과정)를 가짐.
다음 코스는 무조건 존재하는 것이 아니므로 nil을 할당할 수 있게 optional.
course는 department와 nextCourse를 소유하지 않는다. (unowned)
> ARC Ownership Model, 그러니까 ARC 상 소유권을 따져본다고 생각하면 학과가 강의를 소유하고 있지 강의가 학과를 소유하고 있는 것은 아님.
> nextCourse는 학생들이 들어야 하는 다음 강의의 정보이므로(should take) 이번 강의보다 다음에 들어야 함. 근데 영어로 should take니까 사실 강제는 아니고 추천 정도라서 strong 하게 소유할 필요는 없고, 다음에 듣는다는 건 어쨌든 이번 강의보다는 더 나중에 듣게된다는 거니까 nextCourse의 lifetime이 더 길다. 그래서 unowned로 설정.(맞을까 흑흑.. )
Department와 Course를 사용해 구현한 코드. 그림으로 본다면?
각각의 Course 인스턴스들은 Department 인스턴스와 다음 Course 인스턴스를 unowned 참조하고, Department 인스턴스에서는 Course 인스턴스들을 강하게 참조한다.
unowned optional reference는 인스턴스를 strong하게 참조하는 것이 아니기 때문에 인스턴스가 메모리에서 해제되는 것을 막을 수는 없다. 참조 값이 nil이 될 수 있다는 점을 빼면 unowned reference와 동일. unowned reference와 동일하게 명시적으로 현재 인스턴스보다 unowned optional reference가 먼저 할당 해제 되지 않도록 해야 하는 책임이 있다. (department.courses에서 코스 하나를 삭제하려면 그 코스를 nextCourse로 참조하고 있는 부분에서도 삭제가 필요하다.)
그렇다면.. weak을 쓰지 않고 unowned optional을 사용하는 이유가 뭘까.
를 질문한 스택오버플로우 글이 있었다. weak이 더 오버헤드가 심하고 그렇다고....
이 글을 쓰면서 머릿속에 정리된 결과로는 weak보다는 소유관계를 명시적으로 보여주고 싶을 때(기본값은 nil이 될 수는 있겠는데, 참조하기 시작한 다음에는 먼저 nil이 되진 않았으면 좋겠어!)를 코드 구문으로 보여주는 것이 아닐까 싶다. 이 개념이 나타나기 전에는 strong optional로 reference count를 1 증가시켜 해제를 못하게 막고 원하는 시점에 직접 nil을 주입하거나 weak optional로 선언해 아무 때나 nil이 될 수도 있음을 인지하고 작성해야 했으니까..? unowned optional로 작성한 코드를 다른 사람이 보면 "아, 이 프로퍼티는 특정 순간에만 nil이 되어야겠구나!"를 추측할 수 있으니까 weak보다는 좋은 것 같다고 생각한다. (맨 위에 얘기한 대로라면 weak은 이런 상황에서는 어울리지 않는다. 인스턴스가 나중에 해제되는 걸 원하는데 weak은 그 반대 개념이라)
Swift 5.0에서 새로 나타난 개념인데 제대로 이해했는지는... 모르겠다 😂
Unowned References and Implicitly Unwrapped Optional Properties
Country는 수도인 capitalCity를 반드시 가져야 하고, City는 어느 나라의 도시인지를 알기 위해 country를 사용해야 하지만 country를 소유하지는 않는다. (도시가 없어져도 나라는 남는다..!) unowned를 사용했기 때문에 strong reference cycle이 발생하지 않는다.
여기서 특이한 점은 Country의 capitalCity가 City!라는 것인데 이것이 City이면 안 되는 이유는 무엇일까?
'self' used before all stored properties are initialized
City를 생성할 때 self를 넘겨주고 있는데, 아직 인스턴스의 값이 모두 초기화되지 않았다는 에러가 난다. 이 부분은 Two phase init의 조건을 만족하지 못해 생겨난다. (관련 글은 여기)
예전에 정리해 둔 init 문서 포스팅이 있어 캡처해 왔다. 이번 오류는 safety check 4번을 만족하지 못해 발생한 에러이다. 그럼 optional인 City?로 표기해야 할 것 같은데 왜 강제 언래핑을 사용했을까?
그것은 바로 Implicity Unwrapped Optionals에 나와있는 대로 capitalCity가 처음 값이 할당된 이후 언제나 값이 있음을 보장하기 때문이다. init에서 바로 초기화된 이후부터는 인스턴스에 항상 값이 존재하므로 강제 언래핑을 사용할 수 있는 것!
이런 방법도 있구나~ 하고 알아두고 있다가 언젠가 이렇게 명확하게 강제 언래핑 + unowned reference를 사용할 순간이 오면 바로 써먹어야겠다.
이번 포스팅에서는 strong reference cycle을 피하는 방법들을 알아봤는데, 메모리 그래프를 잘 확인한 것인지... 또 optional unowned reference를 잘 이해한 것인지 모르겠다ㅠ 개발하다가 찐으로 이해하는 순간이 오면 수정을 해보는 걸로! 일단 어떤 방법이 있는지는 알았으니 선방이다.
'Swift' 카테고리의 다른 글
LocalNotification (0) | 2024.05.17 |
---|---|
ARC(Automatic Reference Counting) - Strong Reference Cycles for Closures (0) | 2023.02.16 |
ARC(Automatic Reference Counting) - What is ARC? (1) | 2023.02.15 |
Self vs self (0) | 2023.02.03 |
Protocols (0) | 2023.02.03 |