안녕하세요.
저번 글에서 [weak self]를 쓸 때 주의할 점에 대해서 알아봤는데요.
비슷한 내용이지만 좀 다른 내용과 예시가 설명되어 있는 글이 있어서, 그 글을 읽고 내용을 정리해보려고 합니다.
## 0. Retain Cycle이 발생하는 예시
우선 retain cycle이 발생하는 예시를 들어볼게요.
그러면 참조 관계는 아래처럼 되어서 retain cycle이 발생합니다.
(클로저도 reference 타입이기 때문이죠!)
이 retain cycle을 해결할 수 있는 방법이 아예 없는 것은 아니에요.
아래처럼 Child.finishedPlaying에 빈 클로저를 넣으면 cycle이 깨지고 메모리가 해제됩니다.
사실... 이렇게 일일이 빈 클로저를 할당하고, 참조 관계를 생각해야 하는 게 쉬운 일은 아닙니다.
아예 이슈가 발생하지 않도록 뿌리부터 뽑아버리는게 가장 좋은 방법인데요,
retain cycle을 피하기 위해서 아래 3가지를 고려해야 한다고 합니다.
1. non-escaping 클로저에서는 retain cycle이 발생하지 않는다.
2. 클로저에서 self를 약하게 참조하면 retain cylce은 절대 발생하지 않는다.
3. 이상한 동작을 피하기 위해 클로저 맨 위에서 weak self -> strong self로 업그레이드시키자.
하나하나씩 어떤 의미인지 살펴볼게요.
## 1. non-escaping 클로저에서는 retain cycle이 발생하지 않는다.
클로저에서 self를 강하게 참조하면 retain cycle이 발생하는 경우가 많지만, 항상 그런 것은 아니에요.
이건 escaping 클로저와 non-escaping 클로저를 따로따로 봐야 합니다.
### 1.1 escaping vs non-escaping 클로저
만약 Child.playLater method의 클로저를 아래처럼 non-escaping 클로저로 수정하면
이런 에러가 발생해요.
이게 무슨 뜻이냐?
지금 playLater method에서는 completion을 finishedPlaying 프로퍼티에 할당해주고 잇죠?
그 말은 즉, playLater method와 completion의 lifetime이 서로 다르다는 의미입니다.
(playLater method가 종료된 이후에도 클로저가 호출이 될 수도 있다는 말이죠.)
그래서 꼭 @escaping annotation을 붙이라고 컴파일러가 말을 해주는 것이구요.
그런데 만약 method와 completion의 lifetime이 동일한 경우(즉, non-escaping 클로저)인 경우라면 어떻게 될까요??
method가 종료되면 non-escaping 클로저도 함께 종료되어서, 클로저가 잡고 있는 강한 참조가 없으므로 retain cycle이 발생하지 않습니다.
즉, non-escaping 클로저에서는 self를 강하게 참조해도 retain cycle이 발생하지 않는다는 것이죠!
### 1.2 non-escaping Child method 구현
처음에 소개한 예시에 non-escaping 클로저를 추가하면,
이렇게 됩니다.
사실 child.play 클로저에서 self를 빼고도 사용할 수 있어요. (retain cycle이 발생하지 않는다는 것을 컴파일러가 알기 때문이죠!)
즉, 클로저 안에서 self를 쓰지 않고도 에러가 발생하지 않는다면, retain cycle이 발생하지 않는다는 것을 의미합니다.
## 2. 클로저에서 self를 약하게 참조하면 retain cylce은 절대 발생하지 않는다.
이번에는 [weak self]를 써볼까요??
[weak self]를 사용하면, escaping 클로저든 non-escaping 클로저든 아래 같은 참조 관계를 가지기 때문에 retain cycle이 발생하지 않습니다.
## 3. 이상한 동작을 피하기 위해 클로저 위에서 guard-let-self 구문으로 업그레이드시키자.
[weak self]를 사용할 때는 일반적으로 아래처럼 guard-let 구문으로 self를 강한 참조로 상향시키는 것을 권장합니다.
그럼 2가지 의문점이 있을 것 같아요...
- guard-let-self가 아니라 guard-let-strongSelf를 사용해서 클로저 내부에서 약하게 참조되어 있는 self를 사용할 수 있게 하는 게 좋은 거 아닌가??
- guard-let-self를 쓰지 않고 그냥 self?를 반복해서 쓰면 안되나???
위 2가지 방법의 단점들에 대해서 알아볼게요.
### 3.1 self 대신 strongSelf를 쓰는 것?
아래같이 guard-let-strongSelf 구문을 사용해서, 클로저 안의 클로저에서 동일하게 약한 참조로 걸려있는 self를 사용하는 코드를 살펴볼게요.
코드 자체에는 문제가 없습니다. retain cycle도 발생하지 않구요.
그런데, 가독성이 많이 떨어져 보이지 않나요??
또한 내부 클로저에서 self가 아닌 strongSelf를 그대로 사용할 경우에는 retain cycle이 발생할 수도 있어서 버그를 일으킬 위험이 있어요. (그래서 권장하지 않습니다...)
### 3.2 self?를 반복해서 쓰는 것?
아래 같이 클로저 내부에서 self?를 반복해서 쓰는 코드가 있다고 가정해볼게요.
문법적으로는 문제가 없지만 결과값은 이상할 수 있어요.
self?를 썼다는 것은 언제든지(그리고 예상하지 못하는 순간에..) 그 인스턴스의 메모리가 해제될 수 있다는 것을 의미해요.
위 코드는 클로저가 동작하기 전에 인스턴스가 해제되었으면 totalPoints = 0을, 클로저가 정상 동작했을 때는 totalPoints = 2를 설정하는 것을 의도했겠지만,
multi-thread 환경을 고려해보면, 1️⃣ 코드 이후 2️⃣ 코드 전에도 인스턴스가 해제될 수 있겠죠??
그럼 totalPoints 값이 0과 2 뿐만 아니라 1도 될 수 있다는 것입니다. (심각한 버그로 이어질 수 있습니다..)
그래서 아래처럼 클로저가 동작하기 전에 인스턴스가 존재하는지 확인하고, 클로저가 동작할 때는 인스턴스가 존재하는 것을 보장하도록 구현하는 것이 좋아요.
## 참고
- https://chrisdownie.net/software/2022/04/10/the-golden-rules-of-weak-self/
이번 글은 여기서 마무리.
'Swift' 카테고리의 다른 글
Modeling errors (0) | 2022.05.27 |
---|---|
클로저에서 [weak self] 사용할 때 주의할 점3 (0) | 2022.05.21 |
클로저에서 [weak self] 사용할 때 주의할 점 (0) | 2022.04.16 |
Automatic Reference Counting (ARC) (0) | 2022.04.10 |
subscript (0) | 2022.04.03 |