본문 바로가기

iOS 개발/App 개발 관련

[iOS] 어떤 클로저에 [weak self] 해야 할까?

이전 포스팅을 통해 강한 참조 순환이 생기는 이유와 해결 방법을 알아보았다. 특히 클로저와 클래스 인스턴스 사이에 생기는 참조 순환은 일상적으로 발견하기 쉬운데 self 를 캡처하는 모든 클로저가 메모리 누수를 일으키는 것은 아니다.

 

이번 포스팅을 통해 어떤 클로저가 실제 메모리 누수를 일으키는지, 따라서 [weak self] 를 작성해야 하는지 확인해보자.

non-escaping  vs.  escaping 클로저

클로저에는 크게 두 가지 분류가 있는데, 이름에서 알 수 있듯 어딘가로 탈출할 것 같은 클로저와 그렇지 않은 클로저이다.

non-escaping 클로저

어디 가지 않고 있을 것 같은 이름의 클로저 즉, 선언되는 즉시 실행된다. 프로퍼티에 저장되거나 추후에 실행되거나 하는 일이 없다. 

 

컴파일러는 non-escaping 클로저가 선언된 procedure (혹은 function) 가 끝나는 (return) 순간에 클로저가 캡처한 모든 객체를 놓아줄 것을 보장한다.

 

즉, non-escaping 클로저는 그를 둘러싼 문맥이 종료되기 전에 마무리 됨이 확실하다는 것이다. (이 때문에 non-escaping 클로저 내부에서는 self 를 명시적으로 사용하지 않아도 된다.)

 

따라서, 이미 종료된 문맥에 클로저의 작업이 남아 있거나 하는 일이 없고 이는 참조 순환이 생기지 않는다는 뜻이다. 실제로 아래에서 소개할 특정 이유로 클로저의 종료를 지연시키는 몇 가지 경우를 빼면 non-escaping 클로저에는 [weak self] 가 필요없다.

Escaping closures

이름처럼 어딘가로 이동한다. 프로퍼티에 저장되거나 다른 클로저에 넘겨져 선언 이후 어느 시점에 실행된다.

 

이러한 클로저에서 self 키워드를 통해 클래스 인스턴스에 강한 참조를 가진다면 강한 참조 순환이 생길 수 있다. 이 경우는 [weak self], [unowned self] 를 통해 '강하지 않게 참조' 함으로써 메모리 누수를 예방해야 한다.

클로저의 할당 해제를 지연시키는 경우

non-escaping 클로저가 [weak self] 를 필요로 하지 않는 이유에 대해 알아봤는데, 어떤 이유로 클로저의 할당 해제를 지연하는 경우에는 [weak self] 를 사용해야 할 수 있다. 

 

지연되긴 하지만 해제되긴 하므로 메모리 누수와는 조금 차이가 있지만 원하지 않는 결과를 가져올 수 있다는 점에서 참조 순환의 고리를 끊을 필요가 있다. 예를 들어, 지연되는 클로저 때문에 어떤 컨트롤러의 해제가 원하는 시점에 이루어지지 않을 수 있다.

 

클로저에서 네트워킹 다운로드 등의 시간이 걸리는 작업을 하거나, 의도적으로 delay 를 준 후 실행되거나, timeout 이 긴 callback 을 받는 등의 작업이 이 경우에 해당한다.

[weak self] and guard let self = self

[weak self] 는 self 를 옵셔널로 만들기 때문에 옵셔널 체이닝 혹은 guard let, if let 등을 통해 바인딩해야 사용할 수 있다. guard let self = self 를 사용하는 경우 클로저의 범위 내에서는 self 에 대한 강한 참조를 보장받는다.

 

즉, 어떠한 이유로 클로져의 할당 해제가 지연되더라도 guard let 으로 바인딩한 self 는 클로저의 범위 내에서 할당 해제되지 않을 것을 보장 받는다. 

 

따라서, 컨트롤러의 할당 해제에 무관하게 클로저 내의 작업이 완료되어야 하는 경우 guard 구문을 사용하고 그렇지 않은 경우 프로퍼티에 접근할 때 self?.someProperty 로 nil 을 확인하는 것이 좋다.

 

 

 

References

You don't (always) need [weak self] | by Besher AI Maleh | Medium