Notice
Recent Posts
Recent Comments
Link
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Archives
Today
Total
관리 메뉴

욤찌의 개발 일기

[Swift] ARC(Auto Reference Counting) (2) 본문

Swift

[Swift] ARC(Auto Reference Counting) (2)

yyomzzi 2023. 7. 17. 11:41

바로 앞 글에 이어서 두번째 ARC 알아보는 시간..~

글이 길어지는 것 같아서 나눈거임,, 귀찮은거 절대 아님,,

그래서 강한 참조 사이클을 해결하기 위해서 어떻게 해야하는가~


💡Resolving Strong Reference Cycles Between Class Instances

클래스 인스턴스 간의 강한참조사이클을 해결하기 위해서 2가지 방법이 존재하는데

바로 약한참조(weak reference)미소유참조(unowned reference)임.

이 방법들을 사용하면 강한 참조를 하지않고도 다른 인스턴스를 참조할 수 있음.

그래서 서로의 인스턴스에 접근은 가능할 수 있으나 중요한 것은 RC 값을 올리지는 않는다!!!

대신 서로를 강하게 참조하지 않기 때문에 서로의 인스턴스를 유지시킬 수 있는 힘은 없음.

그래서 그냥 한 쪽 인스턴스가 해제되면 다른 쪽도 같이 해제됨. 

 

약한 참조와 미소유 참조는 참조하는 인스턴스의 RC를 올리지 않는다는 공통점이 있지만

인스턴스 수명 차이에 따라서 어떤 것을 사용해야 하는지에 대한 차이점이 있다.


1️⃣ 약한 참조(weak reference)

약한 참조는 이름부터가 강한 참조의 반대말임.

약한 참조를 하게 되면 인스턴스를 참조하더라도 RC 를 증가시키지 않음!

그래서 참조하고 있는 인스턴스가 메모리에서 해제가 되면 자동으로 nil로 초기화해줌. 

 

그렇기 때문에 weak 은 무조건! 옵셔널 타입으로, 그리고 var(변수)로 선언해주어야 함!!

왜냐면 참조하고 있는 인스턴스가 메모리에서 해제되면 값을 nil로 바꿔줘야 하니까!!

 

그래서 약한 참조로 선언되는 인스턴스는 상대 인스턴스의 값이 없더라도 괜찮음.

왜냐면 참조하는 인스턴스의 값이 없으면 nil로 할당하면 되니까

그래서 상대방의 인스턴스가 나보다 더 수명이 짧다면 약한참조로 선언하면 됨!

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 이라고 붙여줌.

 

위와 같은 예제에서 Apartment class 의 tenant 값을 weak var 약한 참조로 선언해줌.

그리고 똑같이 변수에 할당해주게쑴

 

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

아까랑 코드상으로는 똑같지만 이제 참조하고 있는 모습이 달라짐.

unit4A!.tenant가 아까와 똑같이 john 을 할당받아서 Person 인스턴스를 참조하고 있지만,

약한 참조를 하고 있기 때문에 Person 인스턴스의 RC 값을 올리지 않음.

그리고 이런 경우에는 한 쪽에만 약한 참조를 해도 강한참조 사이클을 갖게되지 않기 때문에

양 쪽 다 약한 참조를 할 필요는 없음. 

 

이런 식으로 되는거임!! 그래서 만약 한 쪽에 nil을 할당해 보겠음

 

john = nil
// Prints "John Appleseed is being deinitialized"

 

john에 nil을 할당했기 때문에 john과 Person 인스턴스에 대한 참조가 해제됨.

강한 참조일 경우에는 Apartment가 Person을 참조하고 있기 때문에 메모리 해제가 되지 않았지만,

약한 참조에서는 RC값을 애초에 올리지 않기 때문에 john 변수에 nil을 할당함으로써

Person 인스턴스는 메모리 해제가 됨.

그러면 tenant 프로퍼티는 어떻게 되는 것이냥?

weak 을 선언하게 되면 자동으로 nil을 할당해준다고 했잖숨? 그래서 tenant 는 nil 값을 갖게 됨. 

 

 

그런데 위에서 한쪽만 해도 된다고 했잖슴? 이게 이해가 잘 안됐는데 여기서 알 수 있음.

이제 Person이 메모리에서 아예 해제가 되었기 때문에 Person은 Apartment 인스턴스를 참조하지 않게됨.

그래서 결국 남는건 unit4A변수가 Apartment 를 참조하는 것 뿐임. 그래서 Apartment 의 RC는 1임.

 

unit4A = nil
// Prints "Apartment 4A is being deinitialized"

그래서 unit4A 변수에도 nil을 할당하게 되면 이제 더이상 Apartment 인스턴스에 대한 참조가 없기 때문에

자연스럽게 메모리에서 해제된다. 그래서 초기화 해제 구문이 출력된다.

 


2️⃣ 미소유 참조(unowned reference)

미소유 참조와 약한 참조의 기능은 RC를 올리지 않는다는 점에서 똑같으나 한가지 다른 점이 있음. 

 

 

약한 참조와 달리 미소유 참조는 항상 값을 갖도록 예상됩니다. 결과적으로 미소유로 만들어진 값은 옵셔널로
만들어 지지 않고 ARC는 미소유 참조의 값을 nil 로 설정하지 않습니다.

IMPORTANT 참조가 항상 할당 해제되지 않은 인스턴스를 참조한다고 확신하는 경우에만 미소유 참조를
사용합니다. 인스턴스가 할당 해제된 후에 미소유 참조의 값에 접근하려고 하면 런타임 에러가 발생합니다.

 

공식 문서를 보자면 미소유 참조는 상대 인스턴스에 값이 무조건! 있을거라고 예상함.

그 말인 즉슨 상대 인스턴스가 미소유 참조로 선언되는 인스턴스보다 수명이 길어야 한다는 뜻임.

왜냐면 상대 인스턴스에 무조건 값이 있을거라고 예상하고 선언하는게 미소유이기 때문에!

그렇기에 미소유 참조 인스턴스 또한 무조건 값을 가지게 될 거라고 예상된다.

(상대 인스턴스에 무조건 값이 있어서 메모리가 할당되어 있을 것이니까) 

 

 

만약 상대 참조 인스턴스에 값이 없다면 weak 참조는 자동적으로 nil을 할당받겠지만

unowned는 nil을 할당받지 못함. 그 말인 즉슨! nil이 할당되면 안됨.

상대방이 나보다 수명이 짧아서 먼저 죽으면 내가 nil을 할당받아서 그 자리를 유지해야 하는데,

nil을 할당받지 못하니까 이미 해제된 메모리 주소를 들고 있는거임

혹시라도 그러다가 거기 접근하면 값이 걍 없으니까 앱이 죽어버림 ㅜㅜㅜ

그래서 미소유 참조를 사용하려면 상대 인스턴스와 수명이 같거나 내가 더 짧아야 하기 때문에

두 인스턴스 중 수명이 더 긴 인스턴스를 가르키는 인스턴스를 미소유 참조로 선언!

(미소유 참조로 선언되는 애가 더 수명이 짧아야 함)

 

(그런데 사실 미소유 참조를 잘 알지 못하고 사용하는 것은 위험하기는 함. 왜냐면 값이 없는 참조 주소를

가지고 있으니까 ,,!! 혹시 잘못 접근하면 앱이 꺼져서ㅠㅠ 

그래서 이 참조 사이클을 잘 알기 전까지는 왠만하면 약한 참조를 사용하는 것이 좋을듯!)

 

그래서 미소유 참조는 let으로도 선언이 가능함. 왜냐면 값이 안바뀔 수도 있으니까!

약한 참조는 바뀔거라고 확신하는 가정이 있기 때문에 var로만 선언해야함.

 

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

 

 

공식문서에서 예시를 가져옴.

Customer class에서 card라는 저장 프로퍼티는 옵셔널 CreditCard 타입으로 선언되어 있고,

CreditCard class에서 customer은 옵셔널이 아닌 Customer 클래스로 선언되어있음.

여기서 서로 강한참조를 하지 않기 위해서 약한참조를 할 수도 있을 것이여.. 하지만!

Customer은 신용카드를 가질 수도 있고 안가질 수도 있지만,

creditcard는 무조건 고객과 연결이 되어있어야 하기 때문에 customer의 수명이 creditcard보다 길어야 함

그래서 unowned 참조를 쓴거임!


그렇다면 값타입은??? 어떻게 되는건데????

값형식은 메모리 값이 복사되어 전달이 되고, Stack 영역에 저장이 되기 때문에

해당 값을 담고 있는 영역이 종료되면 stack영역에서 자동제거 됨! 그래서 특별히 메모리 관리를 해줄 필요가 없음


결론은!

- ARC(자동 참조 카운팅)은 개발자가 굳이 코드로 하지 않아도 알아서 참조 숫자를 카운팅 해준다

- 그러나 참조 숫자만! 카운팅 하는 것이기 때문에, 강한 참조 사이클을 발생시키는 상황이 나타난다면 메모리 누수가 된다.

- 그래서 강한 참조 사이클을 해결하기 위해서는 약한참조와 미소유 참조를 사용할 수 있다.

- 약한 참조와 미소유 참조는 각 인스턴스의 수명 차이에 따라 그 사용이 달라질 수 있다.

 

 

클로저의 참조 사이클도 이제 공부해야 할 차례..~

넘 어려버서 좀 더 파봐야할듯..~

 

글 내용 중에 잘못된 정보가 있따면 언제나 알려쥬세요 마니 배우고 싶숩니댜!!!

 

**reference는 이전 글과 동일합니다**