안녕하세요, iOS 개발하는 루피입니다.
오늘은 저번시간에 이어 순환 참조를 해결하는 방법에 대해 정리해보려고 합니다.
이전 글 바로가기 : https://kimsangjunzzang.tistory.com/60
바로 시작하겠습니다.
순환 참조를 해결하는 방법
Swift는 순환 참조를 해결하는 방법을 두 가지 제시합니다.
- 약한 참조
- 미소유 참조
이 두 가지 방법을 사용하면, 서로를 강하게 참조하지 않고도 인스턴스 간의 관계를 설정할 수 있습니다. 이를 통해 강한 참조 사이클이 발생하지 않도록 설계할 수 있습니다.
약한 참조
약한 참조는 주로 다른 인스턴스의 수명이 더 짧거나, 먼저 할당 해제될 가능성이 있는 경우 사용됩니다.
Swift에서 약한 참조는 다음과 같은 특징이 있습니다.
- ARC는 약한 참조가 가리키는 인스턴스가 해제되면 해당 약한 참조를 자동으로 `nil`로 설정합니다.
- 따라서 약한 참조는 런타임에 값이 변경될 수 있으므로, 항상 `var`로 선언해야 하며 `let`으로 선언할 수 없습니다.
코드로 살펴보기
class Person {
let name : String
init(name: String) {
self.name = name
print("\(name) 초기화")
}
weak var pet : Pet?
deinit{
print("\(name) 해제")
}
}
class Pet {
let name : String
init(name: String) {
self.name = name
print("\(name) 초기화")
}
var owner : Person?
deinit{
print("\(name) 해제")
}
}
다음과 같이 class를 작성할 수 있습니다. 사람 중에 애완동물을 키우는 사람은 있을 수도 없을 수도 있지만, 애완동물 중에 주인이 없는 케이스는 없다는 가정에서 시작합니다. 즉, 사람은 애완 동물이 없는 시점이 존재 할 수 있지만, 동물은 항상 사람이 있어야 한다는 것입니다.
따라서 Person에 pet 부분을 weak로 선언해 약한 참조가 가능하게 만듭니다.
다음은 구현 부분입니다.
var Tom : Person? = Person(name: "Tom")
var Wolf : Dog? = Dog(name: "Wolf")
Tom?.dog = Wolf
Wolf?.owner = Tom
Wolf = nil
print(Tom?.dog?.name)
// Tom 초기화
// Wolf 초기화
// Wolf 해제
// nil
보이는 것처럼 Wolf에 nil을 할당했을 때, Wolf은 ARC가 0 이 되기에 인스턴스가 메모리에서 해제되는 것을 확인할 수 있습니다.
Tom = nil
// Tom 초기화
// Wolf 초기화
// nil
반면 다음과 같은 경우는 ARC 가 0 이 아니기에 해제되지 않는 것을 확인할 수 있습니다.
그림을 통해 자세히 보겠습니다.
Tom의 ARC는 Tom에 인스턴스를 할당하면서 ARC가 1 증가, 그리고 wolf에서 owner를 강한 참조로 설정했기 때문에 ARC가 1 증가합니다.
-> Tom의 ARC = 2
반면, Wolf의 ARC는 Wolf에 인스턴스를 할당하면서 ARC가 1증가, 그리고 Tom에서 pet을 약한 참조로 설정했기 때문에 ARC는 증가하지 않습니다.
-> Wolf의 ARC = 1
따라서 Wolf에 nil을 할당할 경우 ARC가 0이 되어 메모리에서 해제되지만, Tom에 nil을 할다하면, 아지가 ARC가 1이기 때문에 해제 되지 않는 것을 확인할 수 있습니다.
미소유 참조
미소유 참조는 주로 두 객체의 생명주기가 같거나, 한 객체가 다른 객체보다 더 오래 살아남는 경우 사용됩니다.
Swift에서 미소유 참조는 다음과 같은 특징이 있습니다
- ARC는 미소유 참조가 가리키는 인스턴스가 해제되더라도 해당 값을 자동으로 `nil`로 설정하지 않습니다.
- 따라서 미소유 참조는 항상 값을 갖고 있어야 하며, 옵셔널로 선언되지 않습니다.
- 만약 해제된 객체에 접근하려고 하면 런타임 크래시가 발생합니다.
코드로 살펴보기
class Person {
let name : String
init(name: String) {
self.name = name
print("\(name) 초기화")
}
var pet : Pet?
deinit{
print("\(name) 해제")
}
}
class Pet {
let name : String
init(name: String) {
self.name = name
print("\(name) 초기화")
}
unowned var owner : Person
deinit{
print("\(name) 해제")
}
}
다음과 같이 Class를 작성하였습니다.
구현 부분은 다음과 같습니다.
var Tom : Person? = Person(name: "Tom")
var Wolf : Pet? = Pet(name: "Wolf")
Tom?.pet = Wolf
Wolf?.owner = Tom
Tom = nil
print(Wolf?.owner)
// Tom 초기화
// Wolf 초기화
// Tom 해제
// Fatal error: Attempted to read an unowned reference but object 0x600000c28600 was already deallocated
이 처럼 Tom을 nil로 만든 다음 Wolf의 owner에 접근할 경우 Crash가 발생하는 것을 확인할 수 있습니다.
정리
쉽게 말하면,
난 너가 있어도 되고 없어도 될거 같아 -> weak
너가 없으면 절대 안될거 같아 -> unowned, 너가 없으면 안되기에 nil이 될경우 접근하게 되면 Crash
이렇게 생각하면 좀더 쉽게 이해 할수 있을거 같습니다.
주관적인 생각
사실상 약한 참조와 미소유 참조를 선택하여 사용하는 경우 앱이 Crash 날 위험이 적은 약한 참조를 사용하는 것이 개발하는 입장에서 더 유용하다고 생각합니다. 하지만 인스턴스의 생명주기가 확정적인 경우 미소유 참조를 사용한다면, 협업하는 데 있어 더 명확한 개발이 가능하다는 이점이 있을 거라 생각합니다.
오늘도 화이팅입니다!
'iOS > Swift' 카테고리의 다른 글
[Swift] 제네릭(Generics) (0) | 2025.02.04 |
---|---|
[Swift] ARC 3/3 (0) | 2025.01.30 |
[Swift] ARC 1/3 (0) | 2025.01.28 |
[Swift] Delegate 패턴을 구현해 사용해 보자 (0) | 2025.01.17 |
[Swift] Delegate를 사용해 객체의 동작을 커스텀하기 (0) | 2025.01.16 |