이전 포스트가 있는 글입니다!
[Swift] Swift의 메모리 관리 : ARC 1 →
개요
이전 포스트에서 Swift에서 메모리 관리를 해주는 ARC가 어떤 식으로 메모리를 관리하는 지 확인했다.
Strong Reference, Weak Reference, Unowned Reference 의 세 가지 참조에 대해 알아보았고, Strong Reference Cycle을 해결하는 방법으로 Weak와 Unowned Reference에 대해 알아보았다.
메모리 누수가 왜 일어나는지와 그에 대한 기본적인 해결에 대해서 배웠다면, 이제는 우리가 많이 사용하는 문법에서 발생하는 문제에 대한 해결을 볼 것이다.
Strong Reference Cycles for Closures
Strong reference cycle은 클래스 인스턴스의 속성에 클로저를 할당하고, 그 클로저의 내용(body)이 해당 인스턴스를 캡쳐하는 경우에도 발생할 수 있다.
이 캡쳐는 클로져의 내용이 self.someProperty
와 같이 인스턴스의 속성에 접근하거나 self.someMethod()
와 같이 인스턴스의 메소드를 호출하는 경우에 발생할 수 있다.
둘 중 어떤 경우든 간에, 클로져가 self
를 캡쳐하는 것이 strong reference cycle의 원인이 된다.
이런 strong reference cycle이 발생하는 이유는 클로져가 클래스와 같은 참조 타입이기 때문이다.
어떤 속성에 클로져를 할당하는 말은, 해당 클로져에 대한 참조를 할당한다는 말과 같은 의미이다.
참조를 하게 되면서 위에서 봤던 문제와 본질적으로 같아지게 된다.
두 개의 strong reference가 서로를 할당 해제당하지 않게 도와주는 것과 같다.
그러나 두 개의 클래스 인스턴스와의 관계와는 다르게, 이번에는 클래스 인스턴스와 클로져가 서로를 할당 해제당하지 않게 도와준다.
Swift는 이 문제에 대한 해결책으로 closuer capture list라는 것을 제공한다.
이것에 대해 알기 전에, 클로져에서 왜 cycle이 발생하는지 부터 알고 들어가는 것이 이해에 더 도움이 될 것으로 생각된다.
예제 : 클로져에서 strong reference cycle이 발생하는 이유
아래와 같은 HTMLElement
클래스가 있다고 해보자. ▼
class HTMLElement {
let name: String
let text: String?
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")
}
}
그리고 옵셔널 변수로 paragraph
변수를 만들고, 거기에 HTMLElement
인스턴스를 할당했다. ▼
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
인스턴스 할당을 마치면 아래와 같은 구조로 나오게 된다.
asHTML은 lazy 속성이기에 요청 전까지는 해당 속성의 값은 존재하지 않는다. ▼
그러면 asHTML 메소드까지 호출해보자.
그렇게 되면 아래의 그림과 같이 구조가 그려지게 된다. ▼
print(paragraph!.asHTML())
이제부터 문제점이 드러난다.
여기서 paragraph
의 참조를 끊어버리게 되면, HTMLElement
에 대한 paragraph
의 strong reference가 하나 사라지게 된다.
하지만 HTMLElement
와 클로져가 서로를 strong reference하고 있기에 ARC는 여전히 사용 필요한 인스턴스라고 판단하여 할당을 해제하지 않는다. ▼
paragraph = nil
이런 면에서 앞에서 보았던 두 클래스 인스턴스 간의 strong reference cycle과 본질적으로 같다고 한 것이다.
Strong Reference Cycles for Closures의 해결
클로저와 클래스 인스턴스 간의 강한 참조 순환을 해결하기 위해 클로저 정의의 일부로 capture list를 정의한다.
캡처 리스트는 클로저 본문 내에서 하나 이상의 참조 타입을 캡처할 때 사용할 규칙을 정의하는 것이다.
두 개의 클래스 인스턴스 간의 강한 참조 순환과 마찬가지로, 캡처된 각 참조를 strong reference 대신 weak reference 또는 unowned reference로 선언하기도 한다.
둘 중 어느 것을 선택할 지는 현재 코드가 어떤 상황인지에 따라 적절하게 사용하는 것이 좋다.
self 캡쳐 주의
Swift에서는 클로저 내에서 self의 멤버를 참조할 때 someProperty나 *someMethod()와 같이 *self.someProperty 또는 self.someMethod()와 같이 작성해야 한다.
이렇게 함으로써 실수로 self를 캡처할 수 있다는 것을 기억하도록 도와준다.
Capture List 정의하기
Capture list의 각 항목은 weak 또는 unowned 키워드와 클래스 인스턴스에 대한 참조(예를 들면 self) 또는 일부 값으로 초기화 된 변수(예를 들면 delegate = self.delegate)의 쌍이다.
이런 쌍들은 쉼표로 구분되어 대괄호 안에 배열과 같이 작성이 된다.
Caputre list는 클로져의 매개변수 목록과 반환 타입이 제공되는 경우에, 그 앞에 배치한다. ▼
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
매개변수 목록과 반환 타입이 제공되지 않는다면 리스트 뒤에 in 키워드를 붙여주면 된다. ▼
lazy var someClosure = {
[unowned self, weak delegate = self.delegate] in
// closure body goes here
}
실사용 예시
UIKit을 통해 UI를 구성할 때, 스토리보드를 사용하지 않고 코드로만 작성하려고 노력하곤 한다.
코드로만 작성을 할 때, 클로져를 통해 UI컴포넌트들을 초기화하곤 하는데 예를 들어 UIButton의 경우 아래와 같이 코드가 작성 된다. ▼
let uiButton : UIButton = {
let button = UIButton()
button.setTitle("Button", for: .highlighted)
button.layer.cornerRadius = 20.0
button.backgroundColor = .orange
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(tapped), for: .touchUpInside)
return button
}()
그런데 self
키워드가 사용되었기에 앞에서 보았던 문제가 발생하게 되고, 이 버튼이 사용되지 않게 되었을 때 메모리가 해제되지 않아 메모리 누수가 발생하게 된다.
위의 코드를 아래와 같이 바꾸어 strong reference cycle을 해결할 수 있다. ▼
lazy var uiButton : UIButton = {
[unowned self] in
let button = UIButton()
button.setTitle("Button", for: .highlighted)
button.layer.cornerRadius = 20.0
button.backgroundColor = .orange
button.translatesAutoresizingMaskIntoConstraints = false
button.addTarget(self, action: #selector(self.tapped), for: .touchUpInside)
return button
}()
Weak와 Unowned Reference
Unowned reference를 사용하는 상황
클로져와 인스턴스가 서로를 참조하고, 동시에 할당 해제될 때 클로져의 캡쳐를 unowned reference로 정의한다.
인스턴스가 사라질 때, 클로져도 같이 사라지는 관계라면 unowned가 좋다는 의미이다.
아래와 같이 클래스 인스턴스로 클로져가 있고, ▼
클래스 인스턴스가 먼저 할당 해제가 되는 경우를 보자.
unowned reference는 인스턴스를 유지시키지 않으므로 클래스 인스턴스는 할당 해제가 된다. ▼
클래스 인스턴스가 할당 해제되었으니, 클로져의 strong reference도 사라져서 클로져도 같이 할당 해제가 된다. ▼
평소에 owned reference를 사용하는게 좋은 이유
Swift에서는 아래와 같이 unowned reference를 사용할 것을 권장한다.
“만약 캡처된 참조가 결코 nil이 되지 않는다면,
항상 weak reference 대신 unowned reference로 캡처해야 한다.”
"그런데 이렇게 추천하는 이유가 뭘까? 어차피 weak reference를 사용해도 똑같이 인스턴스와 클로져를 동시에 없앨 수 있지 않나?"
여기에는 3가지 이유가 있다.
- 성능
unowned reference는 weak reference보다 약간 더 빠르다.
weak reference는 참조할 때마다 옵셔널 값을 해제해야 하므로 약간의 오버헤드가 발생한다.
unowned reference는 옵셔널 타입이 아니므로 추가적인 해제 과정이 필요하지 않다. - 코드 가독성
unowned reference는 해당 인스턴스가 항상 존재한다고 가정한다.
따라서 클로저 내에서 해당 인스턴스에 대한 옵셔널 바인딩을 수행할 필요가 없으며, 코드가 간결해진다. - 안전성
unowned reference는 참조하던 인스턴스가 이미 해제된 상태에서 접근하려고 시도하면 런타임 오류가 발생한다.
이는 프로그램의 버그나 예상하지 못한 상황을 빠르게 감지할 수 있게 해준다.
반면 weak reference는 자동으로 nil이 되므로 해당 상황을 감지하기 어렵다.
이런 이유에서 weak로 구현한 것을, unowned로도 구현 할 수 있다면 unowned를 사용하는 것을 권장한다.
Weak reference를 사용하는 상황
반대로 캡처된 클로져가 미래의 어느 시점에서 nil이 될 수 있는 경우 캡처를 weak reference로 정의한다.
weak reference는 항상 옵셔널 타입이며 참조하는 인스턴스의 할당이 해제되면 자동으로 nil이 된다.
이를 통해 클로저의 내용에 존재하는지 확인할 수 있다.
아래와 같이 클로져가 다른 변수를 통해 클래스 외부에서도 사용된다고 해보자. ▼
여기서 클래스 인스턴스가 먼저 할당 해제되어버렸다고 해보자. ▼
클래스 인스턴스가 해제되었어도, 클로져는 아직 참조되기에 사라지지 않고 다른 곳에서 사용도 할 수 있다.
다른 데서 사용하는 경우에, 클래스 인스턴스를 참조하던 참조 변수에 접근하여도 자동으로 nil
로 바뀌어 있기에 nil
체크만 한다면 사용에 큰 지장이 없다. ▼
만약에 위의 예제에서 weak 대신 unowned를 사용했다면, 참조 변수가 자동으로 nil로 바뀌지 않아 접근 시에 치명적인 오류가 발생할 수 있다.
그렇기에 직접 nil로 바꾸는 단계를 넣어줘야 한다. ▼
마치며
ARC에 대한 글은 이번 글로 마무리를 지었다.
우리는 ARC의 표면만 핥은 느낌이긴 하지만, 우리는 iOS나 Swift를 개발하는 사람이 아니기에 그 이상으로 깊게 들어갈 이유는 없다고 생각한다.
물론 더 많이 알아두면 좋지만, 시간은 제한되어있고 이것 외에도 배울 것은 많기에 선택과 집중을 하는게 좋다는 의미다.
ARC에 대한 글을 작성하면서 다른 개념들도 많이 나왔는데, 그 개념들에 대해 약간 두루뭉술하게 기억하고 있는 느낌이라 추가적으로 볼 필요가 있음을 느꼈다.
다들 이 글을 보고 iOS에서 메모리 누수를 잘 막을 수 있었으면 좋겠다~ 다들 화이팅이다~~
'Develop > iOS' 카테고리의 다른 글
[SWIFT] Swift 문법의 기초 (0) | 2024.02.15 |
---|---|
[iOS][ERROR] Button AddTarget 에러 (0) | 2024.02.15 |
[iOS][Swift] 클로져 캡쳐에 대한 이해 (0) | 2024.02.11 |
[iOS][Swift] Two Phase Initialization (0) | 2024.02.09 |
[iOS][Swift] Swift의 메모리 관리 : ARC 1 (0) | 2024.02.09 |