Reference Cycle(with Strong, Weak, Unowned)

DevMinion·2022년 7월 21일
1

Reference Cycle(순환 참조)에 대해서 알아보쟈🙌

Reference Cycle

이전 시간에 ARC에 대해서 포스팅 하면서 Reference Cycle이 나왔었다.
Reference Cycle이란 순환 참조를 말한다. 서로가 서로를 참조하고 있어 말 그대로 Cycle을 이루는 것을 말한다.

Strong Reference Cycle

(Strong Reference Cycle: 강한 순환 참조) 줄여서 강순참. 이는 메모리 누수를 발생시킬 수 있다. Apple의 예제를 참고하여 자세히 알아보자.

class Person {
    let name: String
    var apartment: Apartment?
    
    init(name: String){
        self.name = name
        print("\(name) is being Init")
    }
    deinit {
        print("\(name) is being Deinit")
    }
}

class Apartment {
    let unit: String
    var tenant: Person?
    
    init(unit: String){
        self.unit = unit
        print("\(unit) is being Init")
    }
    
    deinit {
        print("\(unit) is being Deinit")
    }
}

이렇게, 매우 유명한 예제인 Person과 Apartment 클래스를 선언한다. 그 후,
Person인 Minion과 Apartment인 unit4A를 선언하고 초기화한다.

var Minion: Person?
var unit4A: Apartment?

Minion = Person(name: "Minion")
unit4A = Apartment(unit: "4A")


여기까지 진행하면 init()까지 문제없이 동작함을 알 수 있다.

그럼 이제 둘 사이의 참조를 끊어보자. 과연 RC를 0으로 만들어 ARC에 의해 메모리 해제를 할 수 있을까?

Minion = nil
unit4A = nil

두 변수에 모두 nil을 할당하고 돌려보면??!

왜 Deinit이 없지?

Deinit 있어요? 아니 없어요.

문제점

자 강순참의 문제점이 무엇일까. 메모리 그림으로 자세히 알아보자.

위의 코드를 진행하면 Stack영역과 Heap영역은 이런 니낌이다.
여기서 Minion과 unit4A에게 nil을 할당한다면?

이렇게 Stack영역에서는 해제되지만, Heap영역에서 서로가 서로를 강한 참조하고 있기에 ARC에 의해 해제되지 않는다. 심지어 두 영역에 접근할 수 있는 변수인 Minion과 unit4A도 없어졌으니 재시작하기 전까지 메모리 공간을 계속 점유하는 메모리 누수(leak)가 발생한다.

해결 방법?

대표적인 해결 방법으로 WeakUnowned가 있다.
차근차근 알아보자.

Weak

Weak는 약한 참조로 weak를 사용하여 참조하는 것은 RC를 +1하지 않는다. 즉 참조 카운트가 올라가지 않는다는 말씀. 그리고 weak를 통해 참조하던 대상이 메모리에서 해제되면 자동으로 nil을 할당한다. nil을 할당 받는다고? optional이군! 자 이제 직접 코드로 확인하자.

class Apartment {
    let unit: String
    weak var tenant: Person?
    
    init(unit: String){
        self.unit = unit
        print("\(unit) is being Init")
    }
    
    deinit {
        print("\(unit) is being Deinit")
    }
}

Apartment클래스에서 tenant부분을 waek로 선언했다. 자 그럼 컴파일 후 확인해보자.

짜잔! 정상적으로 Minion과 4A가 Deinit된것을 확인할 수 있다.
그림으로도 확인해보자.

이렇게 Apartment의 인스턴스인 unit4A의 tenant가 약한 참조로 Person의 인스턴스인 Minion을 참조한다. RC는 올라가지 않으며(보기 쉽게 +0으로 표시), 만약 Minion을 할당 해제한다면 Apartment의 tenant의 값은 nil이 할당되고 메모리에서 해제된다. 계속해서 해제해보자.

👆Minion에 nil을 할당하여 Person의 강한 참조 하나가 사라짐.

👆Person의 유일한 강한 참조가 사라지고 남은 참조는 Apartment.tenant의 weak참조이기에 메모리에서 해제된 후 Apartment.tenant에 nil을 할당한다.

👆마찬가지로 unit4A에 nil이 할당되어 Stack에서 사라지고 이에 따라 Heap의 Apartment도 RC가 0이된다.

👆이렇게 모든 메모리가 깔끔하게 해제되었다.

특이사항

위에서 사용한 예제에서 Apple은 tenent에 Weak를 선언하여 사용했다. 그럼 이런 의문이 들 수 있다. tenant말고 Person의 apartment에 선언하면 안돼?
물론 가능하다. 직접 코드로 살펴보자.

class Person {
    let name: String
    weak var apartment: Apartment?
    
    init(name: String){
        self.name = name
        print("\(name) is being Init")
    }
    deinit {
        print("\(name) is being Deinit")
    }
}
...
Minion = nil
unit4A = nil


결과를 확인하면, Deinit이 나오는 순서가 바뀐것을 확인할 수 있다. 아까는 Minion init -> 4A init -> Minion Deinit -> 4A Deinit의 순서였다. 이를 그림으로 확인하면,

👆Weak를 apartment에 선언했다.

👆Minion에 nil을 할당한다. 자동으로 Person의 RC는 1이 된다.

👆unit4A에 nil을 할당한다. 자동으로 Apartment의 RC는 0이 된다. Person의 apartment는 Weak이므로 RC를 카운트하지 않는다.

👆RC가 0인 Apartment가 해제되고 Deinit을 출력한다. 참조하던 값이 사라진 Person.apartment는 nil이 할당된다.

👆RC가 0인 Person이 해제되고 Deinit을 출력한다.

위의 순으로 동작한다. Apple에 따르면

두 인스턴스중 다른 인스턴스의 수명이 더 짧은 경우 즉, 다른 인스턴스를 먼저 할당 해제 할 수 있는 경우 약한 참조를 사용한다.

라고 한다. 즉 둘 중에 수명이 더 짧은 인스턴스를 가리키는 인스턴스를 Weak로 선언하면 된다.

Unowned

Unowned의 경우 RC를 증가시키지 않는다는 Weak와의 공통점이 있다. 하지만 Unowned만이 갖는 차이점은 인스턴스를 참조하는 도중에 해당 인스턴스가 메모리에서 사라질 일이 없다고 확신하는 경우 사용한다.
Weak의 경우 참조하던 인스턴스가 사라지면 nil을 할당했다. 하지만 Unowned는 nil을 할당하지 않고 해제된 메모리 주소값을 계속 가지고 있는다.
위에서 사용한 Person / Apartment 예제는 Unowned를 설명하기에 적절하지 않았는지 Apple은 새로운 예제를 제공한다. 바로바로 Customer / CreditCard 예제! 코드를 함께 보자.

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
        print("\(name) is being Initialized")
    }
    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
        print("\(number) is being Initialized")
    }
    deinit {
        print("Card #\(number) is being Deinitialized")
    }
}

var Minion: Customer?

Minion = Customer(name: "Minion")
Minion!.card = CreditCard(number: 1234_5678_9012_3456, customer: Minion!)

해당 예제에서 CreditCard Class에 customer의 unowned가 없다면 강순참을 발생시킨다. 그림으로 직접 확인해보자. 만약 unowned가 없다면,


이렇게 강한 순환 참조가 발생한다. 이제 Unowned를 사용해보자.

👆CreditCard.customer에 unowned 사용.

👆Minion에 nil을 할당하면 스택이 비워지고 참조하던 Customer의 RC가 줄어든다.

👆Customer의 RC가 0이 되어 Heap영역에서 해제된다. 그리고 Weak와의 차이점이 보이는데 CreditCard.customer의 값이 weak를 사용한 경우라면 참조가 사라져 nil을 할당 받지만 unowned는 nil을 할당받지 않고 주소값 그대로 가지고 있는다. 따라서 nil이 아닌 Customer 인스턴스인 Minion의 주소값을 그대로 가지고 있는다.

👆CreditCard의 RC가 0이 되어 Heap영역에서 해제되어 모든 메모리가 비워진다.

Unowned의 경우 Apple에 따르면,

다른 인스턴스의 수명이 동일하거나 수명이 더 긴 경우 사용하라!

라고 한다. 또 unowned는 nil을 할당받지 않기에 let을 사용하여도 상관없다. weak와 비교해보면,

weak var apartment: Apartment?
...
unowned let customer: Customer

이렇게 옵셔널을 사용하지 않아도 괜찮음을 알 수 있다.

Reference(Thx🤙)

Automatic Reference Counting - Apple
[Swift]Automatic Reference Counting 정리 - 민소네

profile
iOS를 개발하는 미니언

0개의 댓글