클로저에서의 weak self — 순환참조 (2)

2023. 4. 12. 15:01iOS/Swift

 

https://mila00a.tistory.com/63

 

ARC를 곁들인 순환참조 (1)

혹은 순환참조를 곁들인 ARC… ARC (Automatic Reference Counting) Swift에서 앱의 메모리를 관리하는 방법 간단하게는 자동으로 메모리를 관리해주는 녀석이라고 할 수 있다. 여기서 ‘메모리’ 란? 우선

mila00a.tistory.com

 

오래 걸렸다. 위 1편에 이어 드디어 순환참조에 대한 두번째이자 마무리 글이다.

 

원래 궁금했던 내용인 왜 클로저 안에서 [weak self] 를 사용해야 하는가를 정리해 보았다.

내용을 적기 전에 공부하면서 참고했던, 클로저에서의 약한 참조에 대해 가장 잘 정리한 블로그 글을 먼저 공유!

 

[weak self] 무조건 사용하는게 맞는걸까? 🤔

클로져에서 self를 캡쳐할 때 를 사용하는 경우는 순환 참조를 방지하기 위해 약한 참조로 클로져 내부에서 해당 클래스의 인스턴스를 사용할때 입니다. 클로져에서 약한 참조를 이용해 특정 인

noah0316.github.io

 

많은 블로그 글 들이 있었지만, 가장 정리가 잘 되어 있었고 무엇보다 나를 이해시켰다.. 대단한 분..

 

그래서 꼼꼼한 정보를 원한다면 위의 글을 참고하는 게 더 도움이 될 것 같고, 나는 이해한 것을 잊지 않기 위해 최대한 개념적으로 풀어서 써보려고 한다.

 

weak self를 쓰는 대략적인 이유

클로저 안에서 self를 사용하면 그 인스턴스를 참조하기 때문에 Reference Count 즉 RC가 증가한다. 그래서 RC를 증가시키지 않는 weak 키워드를 사용하여 self를 쓰면 순환참조를 방지할 수 있기에 보통 ~클로저 안에서 weak self를 쓰면 좋다~ 고 말하는 것이다.

 

💡 함수와 클로저의 차이?

거의 같다고 보면 된다. 다만, 함수는 클로저 안에 속해 있고 이름이 있다는 점이 특징이다.

 

🙋‍♀️ self가 무슨 인스턴스를 참조한다는 건지 헷갈린다면

이건 아래의 예제를 가져왔다.

 

 

Weak self, a story about memory management and closure in Swift

Memory management is a big topic in Swift and iOS development. If there are plenty of tutorials explaining when to use weak self with closure, here is a short story when memory leaks can still happen with it.

benoitpasquier.com

class MyClass {
    func doSomething(_ completion: (() -> Void)?) {
            // do something
            completion?()
     }

     var didSomething: Bool = false
      
     func doEverything() {
      
         self.doSomething { 
              self.didSomething = true // 여기서 self 인스턴스(MyClass 인스턴스)에 대한 강한 참조 발생
              print("did something")
          }
     }
}

 

클래스의 인스턴스 그 자체를 참조하게 되는 것! 좀 더 이해하기 쉬운 예는 ViewController가 될 것 같다. 뷰컨도 클래스를 정의하고 그 인스턴스를 생성해서 사용하니까 뷰컨 안의 클로저에서 self 를 사용한다면 해당 뷰 컨트롤러의 인스턴스가 될 것이다.

 

여기까지는 ARC와 Swift 참조방식에 대해 알고 있는 사람이라면 음 그렇구나 하게 되지만 이제는

그러면 늘 weak self를 쓰지 왜 쓰는 경우가 있고 안쓰는 경우가 있는 거지?

 

하는 의문이 자연스럽게 들게 된다.

다음 단계로 가기 위해서는 escaping 클로저에 대한 이해가 필요하다.

 

@escaping 클로저

클로저를 크게 escaping 클로저와 non-escaping 클로저로 나눌 수 있을 것 같다.

escaping 클로저란 함수의 리턴 이후에 실행되는 함수를 의미한다.

자유로운 만큼 외부 변수에 저장 후 함수 scope 밖에서도 실행이 가능하다.

 

반면, non-escaping 클로저는 함수 내부에서만 사용이 가능하다는 한계가 있다.

 

escaping 클로저는 위와 같은 특성 덕에 비동기적 작업 특히 네트워크 작업에 많이 쓰이는데 아직 사용해 본 경험이 너무 부족해 예시 코드를 작성하는 건 좀 위험할 것 같아서, 말로만 하고 넘어가려 한다.

 

🙋‍♀️ 내가 간단하게 이해한 정도만 언급하자면…

네트워크에서 다운로드가 오래걸리는 데이터를 요청하는 경우를 생각해 볼 수 있다. 그 데이터를 다 받고 나서 실행해야 하는 작업이 있을 것이다. 그 작업을 데이터 요청 함수의 escaping 클로저로 받는 것이다.

그렇게 하면 데이터를 요청하는 함수 자체는 끝이 나도, escaping 클로저로 전달됐던 그 다음 해야 하는 작업은 요청한 데이터를 다운로드 받는 작업이 완료된 이후에 할 수 있다.

자 그래서 결론적으로 escaping, non-escaping을 알아야 하는 이유는 weak self를 언제 써야 하고 언제 쓰지 말아야 할 지 알기 위해서다.

 

non-escaping 클로저는 함수 종료 후 해당 클로저가 사용되지 않는다는 것이 명확하기 때문에 (함수의 scope 밖에서 사용할 수 없음) 순환참조를 일으킬 염려가 없고 weak self를 사용하지 않아도 된다!

 

🙋‍♀️ 근데 나는 이 설명을 보고 self가 클로저보다 먼저 할당해제되면 어떡하지..? 하는 생각이 들었었다.

보통은 클로저가 인스턴스 메소드에서 사용될 것이고, 이런 걱정이 없을 테지만, 만약 클로저가 타입 메소드에서 사용된다면? 하는 질문이 생겨났다.

그래서 찾아 봤는데 타입 메소드에서의 self는 (당연하게도..) 인스턴스가 아니라 타입을 가리킨다고 한다.

또 참조가 발생하는 경우가 애초에 인스턴스를 할당할 때니까 RC는 증가하지 않겠구나.. 하는 깨달음을 얻었다. 배운 건 기억하자.. 나 자신..

아무튼 결론은 non-escaping 클로저는 weak self를 쓸 필요가 없다!

 

그럼 escaping 클로저에서는?

  • 클로저가 객체의 property에 저장되거나 다른 클로저로 전달될 경우에 순환 참조의 위험이 있다.
  • 클로저 안의 객체가 어떤 클로저에 대한 강한 참조를 유지하는 경우에 순환 참조의 위험이 있다.

와..닿진 않지만 어쨌든 강한 참조가 남아있을 가능성이 존재한다는 것이 포인트인 것 같다.

 

weak self를 쓸 필요없는 escaping 클로저

GCD 호출 (예: DispatchQueue) 은 클로저를 인자로 받지만 나중에 실행하기 위해 property에 저장하지 않는 한순환 참조의 위험성이 없다고 한다.

Animation 작업도 마찬가지로 weak self를 사용할 필요가 없다고 한다.

 

💡 Animation이 순환참조가 일어나지 않는 이유

 

iOS) UIView.animate(...) 왜 메모리릭이 발생하지 않나요?

Is it necessary to use [unowned self] in closures of UIView.animateWithDuration(...)? 이 글은 위의 stackoverflow 질문을 읽고 정리해본 글입니다. 작성자님의 질문을 아래와 같았다. 아래의 코드는 메모리 릭을 피할 수

gyuios.tistory.com

이 글을 참고하면, Animation 타입 메소드이기 때문에 weak self를 사용할 필요가 없다고 한다. (아까 내가 했던 고민이 생각나서 나름 뿌듯..?)

 

🙋‍♀️ GCD에 관하여 자세한 글을 보고 싶다면

 

iOS — GCD what the __weak is going on?

We all know that a reference to self in a block should always be done using __weak and not direct to self. Even Xcode warns you that this…

medium.com

영어고, 내용이 많고, 오브젝티브 C로 되어 있고, 기타 등등의 이유로 나중에 다시 공부해야 하겠지만.. 중요한 포인트는 맨 위에 언급한 블로그에서도 나와 있지만 weak self를 쓰느냐 마느냐에 따라 다른 동작 결과가 나타날 수 있다는 것.

 

dispatch queue로 5초 후에 self(인스턴스)의 프로퍼티를 출력한다고 해 보자.

 

weak self를 썼을 때, 인스턴스를 해제시켜 버리면 그 프로퍼티 값은 nil이 된다.

weak self를 쓰지 않았을 때, 인스턴스를 해제시켜 버려도 기존 프로퍼티 값이 잘 출력이 된다.

 

그리고 두 경우 모두 순환참조는 일어나지 않는다. (따로 property에 할당하지 않았기 때문에)

 

어쨌든 위의 GCD를 언급했을 때 알 수 있었듯, 변수에 클로저를 저장하는 경우는 반드시 weak self를 사용해야 한다. 해당 변수를 사용하기 전에 인스턴스가 해제되어 버리면 여전히 RC가 남아있기 때문에 서로 참조를 하고 있고, 서로 할당 해제를 기다리는 상태가 되기 때문에 순환참조가 생겨 버린다.

 

guard let 을 쓸 것이냐, 옵셔널 체이닝(?)을 쓸 것이냐

간단하게 guard let 은 클로저가 종료될 때 까지 강한참조로 self 가 살아있다. (==클로저가 끝날 때 까지 self의 해제를 지연시킨다)

옵셔널 체이닝 self?. 을 사용하면 nil 인 경우를 바로 처리해 동작을 즉시 멈출 수도 있기 때문에 불필요한 작업을 줄일 수 있다.

이건 클로저 종료 전 self가 할당해제 되는 상황인지 (정확하게는 할당해제 되어야 하는 상황인지가 아닐까?) 아닌지를 고려해서 판단해야 할 것 같다.

 

unowned를 쓰지 않는 이유

마지막으로 unowned self 를 쓰지 않는 이유를 간단하게 언급하고 끝내려고 한다.

이전 글에서 Swift의 참조 방식을 공부하며 다음과 같은 내용을 정리했다.

 

weak

  • 더 수명이 짧은 인스턴스에 대하여 약한 참조를 사용한다
  • 참조하는 인스턴스가 해제되면 자동으로 weak 참조를 nil로 설정한다.

unowned

  • 더 수명이 긴 인스턴스에 대하여 미소유 참조를 사용한다
  • 참조하는 인스턴스가 해제되어도 nil로 만들어 주지 않는다 (항상 값이 있다고 예상 → 잘못된 주소로 접근해서 Crash가 일어날 수도 있다)

 

(일단 우선 이건 순전히 나의 생각이다!) 우리가 우려하는 상황은 (weak self를 쓰는 이유는) 강한 참조가 남아있는 상황에서 self가 할당해제 되는 경우다. 그래서 이 경우 self가 더 수명이 짧을 것을 예상하고 사용하는 것이기 때문에 unowned가 아닌 weak 를 쓰는 게 이치에 맞다고 생각했다.

 

그리고 일반적인 (다른 블로그글에서도 언급하는) 이유는 위 정리 내용에서도 언급됐듯이 Crash의 우려 때문이다. 옵셔널 값을 가지는 weak와 달리 안전하게 self를 다룰 수 없다.

 

따라서 unowend self 대신 weak self를 사용하기!

완벽하진 않지만 그래도 순환참조에 대한 공부를 끝냈다!!