정말 오랜만에 글을 쓰네요!!
열심히 논 건 아니고 운영체제 공룡책을 공부하는 폐관수련을 하느라 다른 걸 거의 하지 못했는데, 공부 할 수록 점점 "CS는 무조건 깊게 공부하자!"라고 외치는 개발자가 되어가는 것 같아요. (점점 올드해지는 느낌)
곧 OS에 대한 시리즈와 클린아키텍처에 대한 시리즈도 쏟아져 올라갈 예정이니 많은 관심 부탁드립니당~
최근에 어떤 글을 보았는데, Lazy변수에서 self에 접근하는 경우, self는 값이 있음을 가정하므로 [weak self] 뿐만 아니라 [unowned self]를 사용하여 self에 대해 접근할 수 있다고 하더라고요!
근데 이 말을 보는데 계속 의문이 들더라고요. 과연 안전할까? 물론 시작할 때 있다는 건 보장이 되는데, 실행하는 중간에도 있다는 건 어떻게 보장될까??
만약에 중간에 객체가 사라진다면 thread safe하지 않은 상황에서 안전이 보장이 될까? 싶은거죠!

일단 배경부터 하나씩 설명을 해드릴게요!
왜 약한참조로 받아야하는지부터!
먼저 코드를 보시죠!
class Human {
var name = ""
lazy var greeting: String = {
[unowned self] in
return "Hello, I'm \(self.name)"
}()
lazy var getName: () -> String = { [unowned self] in
return self.name
}
init(name: String) {
self.name = name
}
deinit {
print("Human Deinit!")
}
}
먼저 weak(unowned)를 사용해서 약한참조로 캡처해야하는 건 당연히 맞습니다.
클로저가 Human Class인스턴스를 레퍼런스타입으로 캡처하기 때문에 Reference count가 올라가거든요!
그럼 문제가 무엇이냐...
Human Class 인스턴스는 클로저를 참조하고, 또 클로저는 Human Class 인스턴스(self)를 참조하기 때문에 순환참조 상황이 되어버립니다!!!
그래서 약한 참조로 캡처해서 클로저가 객체를 소유하지 않게해서 Retain이 되는 걸 막아야하는 것이죠!!

근데 lazy이기 때문에 unowned가 항상 안전하다는 말...
과연 맞을까요?
결론적으로는 위험할 수도 있습니다.
위험한 코드를 보여드릴게요!
class Human {
var name = ""
lazy var greeting: String = {
[unowned self] in
return "Hello, I'm \(self.name)"
}()
lazy var getName: () -> Void = { [unowned self] in
DispatchQueue.global().async {
while true {
self.name += "Sdaq"
if self.name.count > 100 { self.name = "Sdaq" }
}
}
}
init(name: String) {
self.name = name
}
deinit {
print("Human Deinit!")
}
}
var sdaq: Human? = .init(name: "Park-Sdaq")
sdaq!.getName()
sdaq = nil
이 코드를 실행시키면 (항상은 아니지만 가끔씩) 다음과 같은 에러를 발생합니다.
Fatal error: Attempted to read an unowned reference but object 0x600003617390 was already deallocatedHuman Deinit!
이 말이 무엇이냐!
Unowned로 갭쳐된 객체가 이미 deinit되어서 댕글링포인터가 되버렸는데 니가 사용해버렸다ㅎ
이 말입니다!!
sdaq = nil
분명히 이렇게 객체를 nil로 만들어주면, 메모리에서 객체가 해제되고, 진행 중인 작업도 해제되어야 하는 것이 맞을텐데 왜 저런 오류가 발생할까요??
바로 deinit되는 시점과 해당 객체를 사용하는 시점에서 Race Condition이 일어나기 때문입니다.
저 코드에서는
나 deinit 될게 작업도 종료할게~~~~ 하는 메세지가 날아감과 동시에
다른 스레드에서는 나 self.name을 참조할게~~~~~ 하는 메세지도 동시에 날아가고 있는거죠
만약 deinit 메세지가 먼저 도착한다면 문제가 없지만, self.name을 사용한다는 메세지가 먼저 도착할 경우에는 문제가 생기는 건데
이미 객체는 nil로 할당되어 클로저 안의 unowned self는 댕글링포인터를 가리키고 있기 때문이죠!
자 그러면 unowned는 어떨 때 쓰는거야??? 라고 고민이 드실 수 있을 것 같습니다.
제가 생각할 때 unowned는,
값이 존재하는 것이 보장될 때 사용하는 것이 아닌,
값을 사용하는 동안 객체가 사라지지 않고 값을 사용 완료한 이후에 deinit이 되는것이 보장될 때 사용하는 것이다!!!!
입니다!
휴~ 오늘도 성장해버렸다
'iOS 개발 > Swift' 카테고리의 다른 글
Swift Hashable을 이해하자 (0) | 2023.12.26 |
---|---|
[Swift] iOS는 화면을 어떻게 렌더링할까? (1) | 2023.10.04 |
[Swift] Struct와 Class를 메모리 원리부터 자세하게 비교해보자 (2) | 2023.02.12 |
[ARC] 성능을 위해 unowned를 꼭 써야할까? (0) | 2023.02.03 |
[Concurrency] Semaphore로 비동기적 이벤트를 동기적으로 발생시키기 (0) | 2023.01.20 |
[ARC] 약한참조(Weak, Unowned)에 대해서 (0) | 2022.11.06 |
[Swift] 지정한 For-Loop 탈출하기 (0) | 2022.10.03 |
[Swift] Stride 함수를 사용하자 (0) | 2022.10.02 |