본문 바로가기

iOS/스터디

[Swift] 클로저에서의 weak self 에 대해 알아보자

300x250

클로저를 사용하면서 weak self를 사용해본 경험이 있거나, weak self를 사용하는 코드를 본 적이 있을 것이다.

weak self 를 왜 사용해야 하고, 언제 사용해야 하는지에 대해 알아보자.

 


1. weak self를 왜 사용하는가?

Weak reference(약한 참조) Retain Cycle(순환 참조) 인한 메모리 릭을 벗어나기 위해 사용한다.

 

Swift Automatic Reference Counting(ARC) 사용하면서 대두분의 참조 문제를 해결해주지만, 가지 이상의 객체가 서로에 대한 Strong Refrence(강한 참조) 상태를 가지고 있다면 Retain Cycle이 발생하게 되며 이때, 메모리 릭이 발생한다.

 

메모리 릭이 발생한다면, 앱에서 Out Of Memory 크래쉬를 내게 되므로 이를 방지하기 위해선 메모리 이슈를 해결해야 한다.

이를 위해 weak을 사용하며, weak reference 다음과 같은 특징이 있다.

  1. 인스턴스를 strong 하게 유지하지 않는다. 따라서, weak referenec가 인스턴스에 대한 참조를 유지 중이여도 deallocate 될 수 있다.
  2. weak reference는 레퍼런스 카운트를 증가시키지 않는다.
  3. weak reference는 optional이며, runtime에도 value를 nil로 만들 수 있다.

 

따라서 self 캡쳐링 하는 상황에서 self 대한 순환참조가 발생하지 않기 위해 weak self 사용한다.

 

 

2. weak self를 언제 사용하는가?

  • 이스케이핑 클로저 안에서 지연할당의 가능성이 있는 경우 (API 비동기 데이터 처리, 타이머 )
    * 이스케이핑 클로저가 아닌 일반 클로저에서는 Scope안에서 즉시 실행되므로 Strong Reference Cycle을 유발하지 않으므로, weak self를 사용할 필요가 없다.
  • 클로저가 객체에 대한 지연 deallocation 가능성이 있는 경우

 

예를 들어 다음과 같이 뷰 컨트롤러가 구성되었다고 생각해보자.

 

import UIKit

class MainViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    @IBAction func pushButtonAction(_ sender: Any) {
        guard let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "TimerViewController") as? TimerViewController else {
            return
        }
        self.navigationController?.pushViewController(vc, animated: true)
    }
}

 

- MainViewController에서는 푸시 버튼을 누르면 타이머 뷰 컨트롤러를 push 하여 보여준다.

 

import UIKit

class TimerViewController: UIViewController {

    @IBOutlet weak var timerLabel: UILabel!
    
    private var timer: Timer = Timer()
    private var repeatCount: Int = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { timer in
            self.repeatCount += 1
            print("Repeat Count: \(self.repeatCount)")
            self.timerLabel.text = "\(self.repeatCount)"
        })
    }
    
    deinit {
        print("Deinit")
        self.timer.invalidate()
    }
}

- TimerViewController에서는 0.1 초마다 타이머가 동작한다.

- 타이머의 동작마다 repeatCount 변수에 1을 더해주고, 해당 값을 timerLabel에 보여준다.

 

위의 프로그램을 동작시켜보자.

 

TimerViewController의 deinit이 호출되지 않고 타이머가 계속 동작한다.

즉, TimerViewController가 release 되지 않았고, 메모리 릭이 발생했다.

 

왜 그런 것일까? TimerViewController의 동작을 살펴보자.

 

클로저 내부에서 self를 캡쳐하게 되면서 reference count가 증가한다.

 

내부적으로 escaping 블록으로 구현되어 있는 scheduledTimer(withTimeInterval:repeats:block:)

 

  1. Navigation Controller에 의해 TimerViewController가 push 되면서 reference count 증가
    (reference count: 1)
  2. scheduledTimer(withTimeInterval:repeats:block:)는 이스케이핑 클로저이고, 클로저 내부에서 self를 strong reference로 캡처해서 가지고 있게 되면서 reference count 증가
    (reference count: 2)
  3. Navigation Controller의 back button을 눌러 TimerViewController를 pop 시키면서 reference count 감소
    (reference count: 1)

이렇게 reference count가 1이 남아있어 메모리 릭이 발생하게 된다.

 

 

이를 해결하기 위해 TimerViewController의 Timer 클로저를 다음과 같이 바꿔보고 실행해보자.

 

timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] timer in
            self?.repeatCount += 1
            print("Repeat Count: \(self?.repeatCount)")
            self?.timerLabel.text = "\(self?.repeatCount)"
})

 

TimerViewController가 deinit 되며 Timer가 invalidate 되었다.

즉, 메모리에서 TimerViewController가 해제되었고, 메모리 릭이 발생하지 않게 되었다.

 

왜 그런 것일까? 다시 한번 TimerViewController의 동작을 살펴보자.

 

weak 으로 self 객체를 참조해온다.

  1. Navigation Controller에 의해 TimerViewController가 push 되면서 reference count 증가
    (reference count: 1)
  2. scheduledTimers 클로저에서 self가 weak reference로 캡처되면서 reference count 유지
    (reference count: 1)
  3. Navigation Controller의 back button을 눌러 TimerViewController를 pop 시키면서 reference count 감소
    (reference count: 0)

이처럼 이스케이핑 클로저 내에서 self를 캡처하게 될 때, weak reference를 사용하여 self를 캡처하면 메모리 릭을 방지할 수 있다.

 

 

또한 클로저 내부에서 self를 임시적으로 strong reference로 가지고 있을 수 있는데, 클로저 내부에서 guard구문을 사용하여 self를 언랩핑 해주는 것이다.

 

timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [weak self] timer in
            guard let self = self else { return }
            self.repeatCount += 1
            print("Repeat Count: \(self.repeatCount)")
            self.timerLabel.text = "\(self.repeatCount)"
})

 

이렇게 해주면, 클로저 내부에서 self를 strong reference로 사용할 수 있고, 외부에서 캡처한 self가 nil이 될 경우 guard 구문에서 return 시키므로 메모리 릭에 대한 걱정 없이 사용할 수 있다.

 

 

 

☞ 예제 소스코드 github


참고자료

https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

https://medium.com/flawless-app-stories/you-dont-always-need-weak-self-a778bec505ef

https://www.youtube.com/watch?v=687KaKJ8B7U 

320x100