문서는 여기 ⬇️
https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html
Strong Reference Cycles for Closures
"strong reference cycle이 발생하는 경우를 설명해 주세요"라는 면접 질문을 받았을 때 앞에서 본 내용들은 당당하게 얘기하고 이건 까먹었다. 어떻게 보면 회사에서 일할 때 제일 많이 보는 구문인데! completion 핸들러에서 [weak self] in을 사용하는 이유를 이번 포스팅에서 알아본다.
클로저는 reference type이다. 우리가 클로저를 사용할 때 클로저 내부에서 self.someProperty에 접근하는 경우가 있다. 이 때 클로저에 별도의 표기를 하지 않는 경우 클로저는 self 인스턴스를 strong 하게 잡게 된다. self 내부에 strong 하게 소유 중인 클로저가 있는데 이 클로저가 또 strong 하게 self를 소유하게 된다면..? [축하합니다, 당신은 strong reference cycle에 걸려드셨습니다 🎉]라는 문구와 함께 [클로저와 self 인스턴스 둘 다 메모리에서 영영 살아남아 행복하게 살았답니다]의 결말이 되어버린다.
이런 상황을 피하기 위해 Swift에서는 'elegant solution'을 제공하는데 그것이 바로 클로저 캡처 리스트(closure capture list)이다. 이 부분에 대해 설명하기 전에 클로저에서 발생할 수 있는 strong reference cycle 코드를 확인해보자.
class HTMLElement {
let name: String
let text: String?
// HTMLElement 인스턴스가 완전히 초기화 된 이후에 lazy에 접근할 수 있기 때문에 self 참조 가능
lazy var asHTML: () -> String = {
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
HTMLElement라는 클래스가 있고, 클래스의 프로퍼티 중 asHTML이라는 클로저가 보인다. (strong) 그리고 클로저 내부에서 self에 접근하는 코드가 있다.
인스턴스를 만들어 asHTML을 호출한 후 인스턴스를 할당해 둔 변수를 nil로 만들면 어떻게 될까? deinit 함수가 호출이 될까?
예상했던 대로 dinit의 print는 출력되지 않는다. 왜냐하면
asHTML 클로저 내부에서 self를 capture 하고(HTMLElement 인스턴스를 strong 하게 잡음), HTMLElement 인스턴스에서도 asHTML 프로퍼티를 strong 하게 참조하기 때문에 paragraph 화살표 하나가 사라졌다고 해서 reference count가 0이 되지 않기 때문이다.
Resolving Strong Reference Cycles for Closures
그렇다면 클로저를 사용할 때 strong reference cycle이 발생하지 않게 하려면 어떻게 해야 할까? 바로 위에서 말했던 클로저 캡처 리스트를 표기하는 것이다.
Defining a Capture List
closure 내부에서 self를 사용할 때 우리는 캡처해 둔 클래스 인스턴스를 사용하게 된다. [weak self]...로 사용하는 부분이 바로 self를 캡처한다는 뜻인데, []로 작성된 것에서 알 수 있듯 클로저 내부에서 사용할 캡처 인스턴스들을 list처럼 나열할 수 있다. 그래서 이걸 capture list라고 부른다. [클로저 내부에서 참조할 인스턴스들이 뭐가 있는지, 어떤 방식으로 참조할지 알려드림~] 정도로 설명할 수 있지 않을까?
// parameter list와 return type이 있는 경우
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
// parameter list와 return type이 없는 경우
lazy var someClosure = {
[unowned self, weak delegate = self.delegate] in
// closure body goes here
}
클로저의 가장 앞부분에 캡처 리스트를 작성해 주면 된다. 그러면 클로저가 생성되는 시점의 인스턴스 상태를 그대로 캡처해서 사용하게 된다.
Weak and Unowned References
weak으로 쓰는 경우
captured reference가 추후에 nil이 될 수도 있는 경우. weak으로 사용하면 언제나 optional type으로 클로저 내부에서 옵셔널 체이닝을 통해 접근. nil이 될 수 있으므로 클로저 내부에서 nil인지를 체크해야 할 수 있다.
unowned로 쓰는 경우
클로저와 captured instance가 always 서로를 참조하고 always 동시에 메모리에서 해제되는 경우. 또한 captured reference가 nil이 될 일이 없을 때 weak보다 unowned로 사용하는 걸 추천한다. (그럼 옵셔널 체이닝 안 써도 되니까~)
자 그럼 이제 캡처 리스트를 사용해서 위 코드의 클로저를 바꿔본 다음 실행해 보자.
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
클로저 내부에서 캡처 리스트에 unowned로 self를 참조하겠다고 표기했다.
짠! deinit 코드가 실행 됐다!
클로저보다 self가 더 오래갈 것이기 때문에 내부에서 self를 unowned로 참조했다. 클로저 코드에서 unowned로 참조하기 때문에 HTMLElement reference count는 paragraph에서 올린 1 뿐이라 해당 변수를 nil로 처리하면 메모리에서 해제가 된다.
이렇게.. 3 포스트에 걸쳐 Automatic Reference Counting 정리해 봤다. 면접에서 정말 잘 나오는 질문이기도 하고.. 그럼에도 불구하고 항상 횡설수설하는 게 현타 왔었는데 이번 정리로 조금은..!! 더 논리적으로 설명할 수 있을 것 같다. 코드에서도 무작정 weak만 쓰지 말고 weak과 unowned를 구분해서 써야지!
'Swift' 카테고리의 다른 글
Sheet와 FullScreen은 공존할 수 없다 (0) | 2025.02.02 |
---|---|
LocalNotification (0) | 2024.05.17 |
ARC(Automatic Reference Counting) - 강한 순환 참조와 Weak, Unowned (0) | 2023.02.16 |
ARC(Automatic Reference Counting) - What is ARC? (1) | 2023.02.15 |
Self vs self (0) | 2023.02.03 |