우리는 무의식적으로 [weak self]를 활용할 때가 매우 많습니다. 흔히 [weak self]를 활용하는 이유를 메모리 릭이라고 합니다. 그렇다면, 우리는 항상 [weak self]를 활용하면 될까요?
[weak self]를 언제 사용하고, 무엇인 지 공부해 보겠습니다.(weak를 남발하는 것의 side effect는 다른 글에서 공부해보겠습니다!)
먼저 클로져의 캡쳐 현상에 대해서 간단하게 알아보겠습니다.
클로져의 캡쳐
클로저는 내부에서 외부 변수를 사용할 때, 해당 변수를 클로져 내부적으로 저장합니다. 근데, 해당 변수가 값 타입이든 참조 타입이든지 간에 무조건 memory capture를 합니다.
즉, 클로져 안에서 값 타입인 외부 변수를 수정하면 참조 타입과 같이 변경이 되는 것이죠. 쉽게 말해서 외부 변수의 주소 값이 clsoure에게 캡쳐되는 것입니다. 그래서 외부 변수를 closure 안에서 계속해서 참조를 합니다.
그렇다면 이러한 클로져의 캡쳐 현상을 막아줄 수 있는 방법이 무엇이 있을까요?
캡쳐 리스트를 활용하면 됩니다.
캡쳐 리스트란 closure가 정의되는 시점에 복사되는 변수들의 리스트를 이야기합니다. 해당 리스트에 나열된 변수의 복사본들은 closure가 메모리에서 소멸되는 순간까지 내용이 변경되지 않은 채로 유지됩니다.
즉, closure 안에서 활용할 복사본을 만드는 것이고 보면 됩니다.
근데, 알다시피 값 타입의 경우 복사본을 만들면 서로 다른 stack 메모리에 올라가게 되지만, 참조 타입은 복사본을 만들더라도 주소 값을 넘겨주기에 우리가 원하는 value semantics를 활용할 수 없습니다.
참조 타입의 캡쳐 리스트를 만들 때는 주소 값을 넘겨주므로 캡쳐 리스트에 대한 reference count가 1 증가합니다.(물론 캡쳐리스트를 안 만들어도 똑같이 올라갑니다.) 그러나, weak나 unowned를 활용할 경우, reference count의 증가를 막을 수 있습니다. 그래서 순환 참조에 의한 원치 않은 메모리 릭을 방지할 수 있습니다.
그렇다면 다시 돌아와서 클로져 안에서 외부 변수를 활용할 때는 항상 [weak self]를 사용하면 될까요?
[weak self]를 붙이지 않아도 되는 경우를 먼저 알아보겠습니다.
함수가 끝나기 전에 실행되는 클로져를 우리는 non escaping closure라고 부르죠. Non escaping closure의 경우에는 함수가 끝나기 전에 실행이 되므로 함수가 끝날 때에는 클로져도 메모리 할당 해제가 됩니다. 따라서 특수한 경우를 제외하고는 [weak self]를 사용할 이유가 없습니다. 쉽게 말해서 flatMap, filter와 같은 고차함수에서 [weak self]를 사용하지 않는 이유와 같다고 보시면 됩니다. (앞서 말한 특수한 경우는 closure가 self보다 먼저 deallocate되면 [weak self]를 추가하지 않아도 되는 상황입니다. - 밑에 예시가 존재합니다.)
또 다른 예는 아래의 코드를 통해서 이해해 보겠습니다.
class Test {
var num = "0"
func start() {
// 1초 뒤에 실행되는 @escaping closure
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let number = self.num
print(number)
}
}
deinit {
print("Deinit이 되었습니다.")
}
}
var test: Test? = Test()
test?.start()
test?.num = "7"
test = nil
/*
7
Deinit이 되었습니다.
*/
먼저 Test 타입의 객체를 만듭니다. 그리고 start() 메소드를 호출하여 비동기 코드를 실행시킵니다. 그리고 비동기 코드 안에서는 객체 내의 num을 참조하여 프린트합니다. (여기서 closure의 캡쳐 현상에 의해서 값 타입인 num을 memery capture 합니다.) 그리고 객체의 num을 변경합니다. 그리고 변수에 nil을 할당합니다.
코드의 맨 밑의 프린트 문을 보면 알 수 있듯이 Test 객체는 메모리 해제가 일어났습니다.(메모리 릭이 발생하지 않았습니다.) [weak self]를 사용하지 않았는데도 말이죠. 왜 그럴까요?
print 문의 순서에서 볼 수 있듯이 deinit(객체의 메모리 할당해제)은 비동기 closure가 끝나기 전까지 일어나지 않습니다. 그 이유는 test = nil이 실행되어도 실제 test 인스턴스는 비동기 closure 속 self로 캡쳐가 되어서 reference count가 1인 상태입니다. 따라서 할당 해제가 일어나지 않는 것입니다. 그리고 closure가 끝나서야 self의 reference count는 0이되므로 그제서야 reference count가 0이 된 test 인스턴스는 할당해제가 되는 것이죠.
그러나 아래의 경우에는 이야기가 다릅니다.
class Test {
var num = "1"
var myClosure: ((String) -> ())? = nil
func start() {
myClosure = {
let number = self.num + $0
print(number)
}
myClosure!("23")
}
deinit {
print("Deinit이 되었습니다.")
}
}
var test: Test? = Test()
test?.start()
test = nil
/*
123
*/
위의 코드는 아래의 print문을 보면 알 수 있드시 메모리 할당 해제가 일어나지 않았습니다. 즉 메모리 릭이 발생한 것이죠! 왜 메모리 릭이 발생했는지 순서대로 한번 봐보겠습니다.
Test 객체가 생성되고 start() 메소드가 실행되면 인스턴스 내의 프로퍼티 myClosure에 클로져를 할당해 줍니다. 이렇게 될 경우, myClosure는 self를 캡쳐하게 됩니다. 그러면 test instance는 myClosure가 참조를 하고 있으므로, reference count가 증가하게 됩니다. 즉 reference count가 1이 아니라 2가 되는 것이죠. 그래서 test=nil을 해도 reference count가 1이므로 메모리에서 할당해제가 되지 않는 것입니다!
그렇다면 이럴 때 reference count를 1 증가시키는 것이 아닌 캡쳐 리스트를 활용하여 약하게 참조를 하면 어떻게 될까요?
import Foundation
class Test {
var num = "1"
var myClosure: ((String) -> ())? = nil
func start() {
myClosure = { [weak self] in
let number = (self?.num ?? "") + $0
print(number)
}
myClosure!("23")
}
deinit {
print("Deinit이 되었습니다.")
}
}
var test: Test? = Test()
test?.start()
test = nil
/*
123
Deinit이 되었습니다.
*/
[weak self]를 통하여 myClosure는 self를 약하게 참조합니다. 따라서 test 객체의 reference count가 증가하지 않죠. 따라서 nil을 할당하면 바로 할당 해제가 일어나게 됩니다.
근데, 위의 코드에서 closure가 self보다 먼저 deallocate된다면, 굳이 [weak self]를 붙히지 않아도 됩니다. 아래와 같은 코드가 바로 해당 예시입니다.
class Test {
var num = "1"
var myClosure: ((String) -> ())? = nil
func start() {
myClosure = {
let number = self.num + $0
print(number)
}
myClosure!("23")
}
deinit {
print("Deinit이 되었습니다.")
}
}
var test: Test? = Test()
test?.start()
test?.myClosure = nil
test = nil
/*
123
Deinit이 되었습니다.
*/
[weak self]는 메모리 릭을 방지해 줄 뿐만 아니라 코드에서 사용했을 때와 사용하지 않았을 때 서로 다른 결과를 도출해 낼 때가 있습니다.
아래의 예를 한번 봐보겠습니다.
class BigTest {
let smallTest: SmallTest
init() {
smallTest = SmallTest()
}
func startSmallTest() {
smallTest.start()
}
deinit {
print("BigTest deinit이 되었습니다.")
}
}
class SmallTest {
var num = "0"
func start() {
// 1초 뒤에 실행되는 @escaping closure
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let number = self.num
print(number)
}
}
deinit {
print("SmallTest가 deinit이 되었습니다.")
}
}
var bigTest: BigTest? = BigTest()
bigTest?.startSmallTest()
bigTest = nil
/*
BigTest deinit이 되었습니다.
0
SmallTest가 deinit이 되었습니다.
*/
BigTest는 SmallTest 객체를 프로퍼티로 가지고 있고, startSmallTest 메소드를 통해서 SmallTest의 start() 메소드를 실행시킵니다. 그리고 bigTest = nil을 통하여 BigTest의 메모리를 할당해제합니다. BigTest가 메모리 할당이 일어나도 SmallTest 객체는 closure의 캡쳐 현상에 의해서 reference count가 0이 아니므로 바로 할당해제가 되지 않고, @escaping closure가 실행되고 나서야 메모리에서 할당 해제가 일어납니다.
즉, 위에서는 두 객체 다 정상적으로 메모리에서 할당해제가 일어나게 됩니다.
그렇다면 아래의 예는 어떻게 될까요?
class BigTest {
let smallTest: SmallTest
init() {
smallTest = SmallTest()
}
func startSmallTest() {
smallTest.start()
}
deinit {
print("BigTest deinit이 되었습니다.")
}
}
class SmallTest {
var num = "0"
func start() {
// 1초 뒤에 실행되는 @escaping closure
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
let number = self?.num
print(number)
}
}
deinit {
print("SmallTest가 deinit이 되었습니다.")
}
}
var bigTest: BigTest? = BigTest()
bigTest?.startSmallTest()
bigTest = nil
/*
BigTest deinit이 되었습니다.
SmallTest가 deinit이 되었습니다.
nil
*/
위와 같은 코드의 경우, 두 객체다 제대로 메로리에서 할당 해제가 일어나지만 print 되는 값이 조금 다릅니다. 첫 번째 코드에서는 0이 출력되었지만, 두 번째 코드에서는 nil이 print 되었습니다.
그 이유는 클로져가 SmallTest를 약하게 참조하고 있으므로 BigTest가 할당해제가 일어났을 때 같이 SmallTest 객체도 메모리에서 할당 해제가 일어나게 됩니다. 따라서 클로져 안에서 self는 nil이 되고 print도 nil이 되는 것입니다.
이렇게 [weak self]를 쓰냐 안 쓰냐에 따라서 서로 동작하는 방식이 다르다는 것도 알게 되었습니다.
다음번 글에서는 weak를 남발하면 어떻게 되는 지에 대해서 알아보겠습니다.
'Swift' 카테고리의 다른 글
[Swift] Async, Await(1) (feat. WWDC) (0) | 2023.08.22 |
---|---|
[Swift] WWDC21 - ARC in Swift: Basics and Beyond (0) | 2023.08.15 |
[Swift] WWDC16 Understanding Swift Performance(3) (0) | 2023.08.03 |
[Swift] WWDC16 Understanding Swift Performance(2) (0) | 2023.07.31 |
[Swift] WWDC16 Understanding Swift Performance(1) (0) | 2023.07.31 |