티스토리 뷰

Swift

[Swift] 클래스 강한 순환 참조

Hani_Levenshtein 2021. 5. 17. 20:06

안녕하세요 Hani입니다.

저번 ARC포스팅 에서는 클래스의 인스턴스 간 상호 참조에 의해 참조 횟수가 0으로 내려가지 않는 문제점이 발생했었습니다.

이번에는 그에 대한 해결 방안을 다뤄볼까 합니다.

 

 

사실 저번 포스팅에서 강한 순환 참조(Strong Reference Cycle)에서 언급을 하지 않고 얼렁뚱땅 넘어간 게 있어요.

그건 바로 Strong🔥이라는 키워드입니다..! 

 

 강한 순환 참조의 첫 번째 해결 방안은 강한 참조를 약한 참조로 만드는 것입니다.

 

코드를 보면서 얘기해볼게요.

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") }
}

클래스의 프로퍼티가 weak var로 선언되었죠?

이 클래스들로 인스턴스를 한번 만들어보고 뭐가 다른지 알아보겠습니다.

 

 

 

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
//Person(name: "John Appleseed")'s count = 1

unit4A = Apartment(unit: "4A")
//Apartment(unit: "4A")'s count = 1

john!.apartment = unit4A
//Apartment(unit: "4A")'s count = 2

unit4A!.tenant = john
//Person(name: "John Appleseed")'s count = 1

Apartment 클래스 안에서 weak var tenant: Person? 로 선언했었죠?

약한 참조를 하게 되면 변수는 인스턴스의 주소를 참조하고는 있지만 인스턴스를 소유하지는 않는 상태가 됩니다.

따라서 약한 참조는 참조 횟수를 증가시키지 않아요. 👍 (retain도 release도 발생시키지 않습니다.)

 

 

 

이제 클래스의 인스턴스들을 없애보도록 하겠습니다.

john = nil
//Person(name: "John Appleseed")'s count = 0
//Apartment(unit: "4A")'s count = 1
//Prints "John Appleseed is being deinitialized"

john 변수가 Person 인스턴스를 참조하지 않으면 release 되어 참조 횟수가 0으로 됩니다.

참조 횟수가 0으로 됐으니까 Person 인스턴스도 메모리에서 사라지게 되겠죠..

 

게다가 Person 인스턴스의 apartment 변수가 Apartment 인스턴스를 참조하고 있던 것도 끊어지게 됩니다.

 

Apartment 인스턴스의 weak tenant 변수는 Person 인스턴스를 참조하고 있었지만 메모리에서 사라져 버렸죠?

따라서 weak tenant 변수가 nil을 참조하게 됩니다.

참조하는 게 nil이 되었어요.

즉, weak 키워드를 붙인 변수는 반드시 Optional로 선언되어야 합니다.

그리고 값이 nil로 바뀔 수 있으니까 weak let 이런 것도 안 되겠죠? var로 선언해야 합니다.

위에서 weak var tenant: Person? 로 잘 선언한 것을 볼 수 있습니다. 

 

 

unit4A = nil
//Apartment(unit: "4A")'s count = 0
//Prints "Apartment 4A is being deinitialized"

weak 키워드 덕분에 성공적으로 인스턴스를 메모리에서 해제했고, 강한 순환 참조도 발생하지 않았습니다. ☺️

 

 

weak 말고도 강한 순환 참조를 막을 수 있는 키워드가 하나 더 있습니다. 보러 가실게요. 

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") }
}

새로운 키워드인 unowned가 등장했습니다. 미소유 참조라고 부릅니다.

이어서 보겠습니다.

 

var john: Customer?

john = Customer(name: "John Appleseed")
//Customer(name: "John Appleseed")'s count = 1
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
//CreditCard(number: 1234_5678_9012_3456)'s count = 1

각 클래스의 인스턴스를 생성하고 참조시킨 모습입니다.

unowned 키워드는 weak과 마찬가지로 참조 횟수를 증가시키지 않습니다.

 

 

john = nil
//Customer(name: "John Appleseed")'s count = 0
// Prints "John Appleseed is being deinitialized"

//CreditCard(number: 1234_5678_9012_3456)'s count = 0
// Prints "Card #1234567890123456 is being deinitialized"

john변수의 Customer 인스턴스를 참조를 끊어버린 모습입니다.

Customer 인스턴스의 참조 횟수가 0이 될 테니 Customer 인스턴스는 메모리에서 사라지겠죠?

Customer 인스턴스가 사라지면 CreditCard 인스턴스를 참조할 변수가 사라지니 CreditCard 인스턴스의 참조 횟수도 0이 됩니다.

따라서 두 인스턴스가 강한 순환 참조를 일으키지 않고 모두 메모리에서 사라지게 됩니다. 

 

 

엥 그럼 weak이랑 unowned가 무슨 차이가 있는데?

 

 

weak은 참조하는 인스턴스가 자기보다 먼저 메모리에서 사라지면, 참조하는 값을 nil로 바꿔주죠? unowned는 그렇지 않습니다.

unowned는 참조하는 인스턴스보다 먼저 죽을 거라 생각될 때 쓰기 때문에 nil로 바꿔주지 않습니다.

아, weak이든 unowned든 같은 타이밍에 메모리에서 사라지는 것은 괜찮아요.

 

 

근데 만약 unowned가 참조하는 인스턴스가 만약 먼저 메모리에서 사라지면?

참조하는 값을 nil로 바꿔주진 않고 댕글링 포인터(Dangling Pointer)로 남습니다.

 

댕글링 포인터는 포인터가 가리키는 인스턴스가 메모리에서 할당 해제되었지만, 포인터가 가리키는 값은 변하지 않아서 여전히 할당 해제된 메모리의 주소를 가리키고 있는 포인터를 말합니다.

 

댕글링 포인터가 가리키는 곳에 액세스 할 경우 런타임 에러가 나겠죠?

 

 

음.. 그래 weak은 참조하는 인스턴스가 죽으면 참조하는 값을 nil로 변환해주니 안심할 수 있고

 unowned는 참조하는 인스턴스가 죽으면 참조하는 값을 바꿔주지도 않고 건드리면 크래쉬도 발생하네..

그럼 weak가 무조건 좋은가?

 

 

weak은 weak 포인터가 가리키는 메모리 주소에 인스턴스가 제대로 있는지 추적하기 위한 오버헤드를 필요로 합니다.

unowned는 unowned 포인터가 가리키는 메모리 주소에 반드시 인스턴스가 있다고 가정을 하니까 추적을 하지 않아요.

 

저는 일단 weak만 쓰겠습니다.. 😅 

unowned를 쓸 실력이 될 쯤에 아래 내용을 추가할까 합니다. 

 

추가할 내용..

unowned (var/let) 

unowned optional

Implicitly Unwrapped Optional


References

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

 

댓글