매번 코딩을 하다가 `[weak self]`를 작성하는 경우가 많았는데요.
어느순간 왜 얘를 써야하지? 매번 이 친구를 써야할까? 라는 생각이 들어서
조금 상세하게 알아보고자 포스팅을 시작하게 되었습니다 :]
1. 클로저는 캡쳐를 한다.
2. Swift의 클로저는 기본적으로 값 복사가 아니라 참조를 한다.
3. 참조를 할 때 강한 참조 순환이 일어나 메모리 누수가 발생 할 수 있다.
4. 그래서 [weak self]를 작성해준다.
5. 하지만 클로저에 작성하지 않아도 되는 상황이 있다.
1. 클로저는 캡처를 한다.
기본적으로 클로저는 주변 상수와 변수를 강한 참조를 통하여 캡처합니다.
그렇기 때문에 클로저는 상수와 변수를 정의한 원래 범위가 더이상 존재하지 않더라도
본문 내에서 해당 상수와 변수의 값을 참조하고 수정할 수 있습니다!
2. Swift의 클로저는 기본적으로 값 복사가 아니라 참조를 한다.
var value = 10
let closure = {
print(value)
}
value = 20
closure() // 출력 결과: 20
클로저가 만약 값 복사를 한다면? 출력결과는 10이였을 것입니다.
하지만 출력결과가 20이기 때문에 클로저는 Value / Reference 타입에 관계없이
캡처할 때 값 복사가 아니라 참조를 한다는 것을 알 수 있습니다.
물론 값 복사가 불가능 한 것은 아닙니다.
`Capture List`를 활용하면 값 복사를 할 수 있습니다.
var a = 0
var b = 0
let closure = { [a] in
print(a, b)
}
a = 10
b = 10
closure() // Prints "0 10"
한가지 주의할 점은 캡처 리스트를 통한 캡처를 했다면 `Const Value Type`으로 캡처합니다.
즉, 상수로 캡처를 했기 때문에 클로저 내부에서 a에 대한 값 변경은 불가능 합니다.
그렇다면 `Reference Type`도 캡처리스트를 통해 Value 캡처가 될까?
공식문서에 따르면 캡처된 변수의 타입이 `Reference Type` 이라면 값 캡처가 아니라 참조가 됩니다.
class SimpleClass {
var value: Int = 0
}
var x = SimpleClass()
var y = SimpleClass()
let closure = { [x] in
print(x.value, y.value)
}
x.value = 10
y.value = 10
closure()
// Prints "10 10"
3. 참조를 할 때 강한 참조 순환이 일어나 메모리 누수가 발생 할 수 있다.
결국 클로저의 캡처가 발생할 때 참조를 한다면 클로저와 인스턴스 사이에 강한 참조 사이클이 생성됩니다.
Swift는 캡처 리스트를 사용하여 이러한 강한 참조를 방지하는 방법을 제공합니다.
실제로 코드를 작성할 때 클로저 내부에서 self에 접근을 하는 경우가 많습니다.
예를들면,
인스턴스의 프로퍼티에 접근하거나 메서드에 접근하는경우
`self.~~`를 사용하여 self를 캡처하여 강한 참조 사이클을 생성하게 됩니다.
간단한 예제를 들어 설명하자면,
class Person {
let name: String
let money: Int?
lazy var getMoney: () -> String = {
if let money = self.money {
return "\(self.name)은 \(money)원을 소유중입니다."
} else {
return "\(self.name)은 돈이 없어요 ㅠ"
}
}
init(name: String, money: Int? = nil) {
self.name = name
self.money = money
}
deinit {
print("\(name) is being deinialized")
}
}
위와 같이 `Person`이라는 객체가 있습니다.
`name`과 `money`라는 프로퍼티를 가지고 있고,
`getMoney`라는 클로저도 지연 프로퍼티로 가지고 있습니다.
`getMoney`는 `name`과 `money`를 하나의 문자열로 출력해주는 클로저를 참조합니다.
왜 lazy하게 선언 되었냐면 `Person`이라는 객체가 생성이 완료되고
self에 접근해야하기 때문에 지연프로퍼티로 설정해주었습니다.
var zhilly: Person? = .init(name: "Zhilly", money: 10000)
print(zhilly?.getMoney())
이렇게 `getMoney()`를 호출하는 순간 클로저가 Heap에 할당되며 참조가 발생합니다.
왜 인스턴스가 생성될 때 메모리에 올라가지 않고 호출 될 때 메모리에 올라가고 참조가 발생할까요?
-> 바로 지연 프로퍼티이기 때문입니다.
지연프로퍼티는 호출되는 순간에 메모리에 올라갑니다.
근데 여기서 `getName`프로퍼티에서 self를 강한 참조하고 있기 때문에
여기서 강한 참조 순환이 발생하게 됩니다.
클로저는 여러번 self를 참조하지만 Person 인스턴스에 대해 하나의 강한 참조만 캡처합니다.
zhilly 변수를 nil로 설정하고 Person 인스턴스에 대한 강한 참조를 끊으면
강한 참조 사이클은 Person 인스턴스와 클로저를 할당해제하지 않습니다.
zhilly = nil
Person의 deinit 메시지는 출력되지 않으며
이는 메모리에서 할당해제 되지 않음을 보여줍니다.
4. 그래서 [weak self]를 작성해준다.
위의 코드에서 어떻게 강한 순환 참조를 해결할 수 있을까요?
`getMoney` 클로저에서 self를 캡처하고 있기 때문에
캡처할 때 RC가 증가하게 되어 강한 순환 참조가 발생합니다.
lazy var getMoney: () -> String = { [weak self] in
if let money = self?.money {
return "\(self?.name)은 \(money)원을 소유중입니다."
} else {
return "\(self?.name)은 돈이 없어요 ㅠ"
}
}
클로저에서 `self` 인스턴스를 캡처할 때 캡처리스트를 활용해
weak로 캡처하여 클로저에서의 강한 순환 참조를 방지할 수 있습니다.
물론 `unowned`키워드를 활용해서 강한 순환 참조를 해결하는 방법도 있지만
객체의 생명주기에 따라 예상치 못한 상황이 발생할 수도 있기 때문에
unowned 보단 weak를 지향해야 할 것 같습니다.
5. 하지만 클로저에 작성하지 않아도 되는 상황이 있다.
1. Escaping 클로저가 아닌 경우
map과 같은 non escaping 클로저
클로저가 escaping이 아니라면 컴파일러는 method가 종료되고나서
해당 클로저가 사용되지 않는 다는것으로 간주합니다.
즉, 함수 종료 직전에 무조건 실행되어야 하고,
함수가 종료됨과 동시의 클로저의 사용이 끝나기 때문에
weak나 unowned를 사용하지 않아도 됩니다.
2. GCD
GCD호출은 나중에 실행하기 위해 프로퍼티에 저장하지 않는 한 순환 참조의 위험이 없습니다.
또한 GCD와 마찬가지로 `UIView.Animate` , `UIViewPropertyAnimator` 와 같은 animation call 역시
속성(property)에 저장하지 않는 한 순환 참조의 위험이 없습니다.
하지만 사용하지 않아도 되지만, `[weak self]` 사용 여부에 따라 다른 동작이 발생할 수도 있고,
발생하지 않을 수도 있기 때문에 항상 사용에 주의를 해야할 것 같습니다.
결론
매번 잘 모르고 사용하던 `[weak self]`에 대해서 깊게 공부해볼 수 있었습니다.
클로저에서 발생하는 순환 참조를 해결하기 위해 사용하는
`[weak self]`를 사용할 때에는 객체의 생명주기를 잘 생각해보고 사용하자!
참고
'Swift' 카테고리의 다른 글
Swift - 컴플리션 핸들러를 async로 감싸기 (0) | 2024.01.22 |
---|---|
Swift - Optional과 Optional Binding (1) | 2023.12.29 |
Swift - 타입 캐스팅 톺아보기 (0) | 2023.12.29 |
Swift - ARC 톺아보기 (0) | 2023.12.14 |
iOS 공부하는 중🌱