iOS 개발/Swift

[Swift] weak self, 어떻게 사용해야할까? (2/2)

꽁치대디 2022. 10. 23. 19:32

[Swift] weak self, 어떻게 사용해야할까? (2/2)

 

[Swift] weak self, 어떻게 사용해야할까?

[Swift] weak self, 어떻게 사용해야할까? 클로저를 매개변수로 받아 메소드를 작성하는 경우, 혹은 URLSession 이나 DispatchQueue 를 이용할 때, 클로저 내부에서 클로저 외부의 프로퍼티나 메소드를 사용

trumanfromkorea.tistory.com

앞선 글에 이어서 마저 작성해보도록 하겠습니다.

GCD

GCD 호출은 나중에 실행하기 위해 어딘가에 저장하지 않는 한 순환참조가 발생할 위험이 없습니다.

 

다음 코드와 같이 작업이 바로바로 수행되는 경우에는 [weak self] 를 사용하지 않더라도 메모리 누수가 발생하지 않을 것입니다.

func nonLeakyDispatchQueue() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
        self.view.backgroundColor = .red
    }

    DispatchQueue.main.async {
        self.view.backgroundColor = .red
    }

    DispatchQueue.global(qos: .background).async {
        print(self.navigationItem.description)
    }
}

하지만 다음 코드는 메모리 누수를 유발할 것입니다.

func leakyDispatchQueue() {
    let workItem = DispatchWorkItem { self.view.backgroundColor = .red }
    DispatchQueue.main.asyncAfter(deadline: .now() + 1.0, execute: workItem)
    self.workItem = workItem // stored in a property
}

그 이유는 DispatchWorkItem 의 클로저를 지역변수에 할당하고 있으며, 내부에서 [weak self] 를 이용하지 않고 self 를 참조하고 있기 때문입니다.

프로퍼티에 함수 저장

우리는 클로저를 한 객체에서 다른 객체로 전달하여 프로퍼티에 저장할 수 있습니다.

 

예를 들어, 객체 B 를 객체 A 에게 노출시키지 않으며, A 가 B 에 있는 메소드를 익명으로 호출하고 싶을때를 가정해보겠습니다. (이는 Delegation 의 경량 대안정도로 생각할 수 있습니다.)

 

먼저 어떤 프로퍼티에 클로저를 저장하는 컨트롤러가 있다고 가정해보겠습니다.

class PresentedController: UIViewController {
    var closure: (() -> Void)?
}

위의 컨트롤러를 소유하고 있는 메인 컨트롤러가 있다고 가정하고, 메인 컨트롤러의 메소드 중 하나를 위 PresentedControllerclosure 변수에 저장해보겠습니다.

class MainViewController: UIViewController {
    var presented = PresentedController()

    func setupClosure() {
        presented.closure = printer
    }

    func printer() {
        print(view.description)
    }
}

위 코드는 다음과 같은 특징을 가집니다.

 

  • printer() 는 메인 컨트롤러의 함수이고 이를 closure 라는 프로퍼티에 할당함
  • 함수의 결과가 아니라 함수 그 자체를 할당한 것이기 때문에 () 를 생략한 것

 

위 코드는 self 를 사용하지 않았음에도 강한 참조 사이클을 유발합니다. 왜냐하면 selfself.printer 와 같이 암시되어 있기 때문입니다. 그러므로 클로저는 self.printer 에 대한 강한 참조를 유지하면서, 클로저를 소유하는 presentedController 도 소유하게 됩니다.

 

이런 강한 참조 사이클을 끊어내기 위해 printer 함수를 호출하는 클로저를 대입하여 코드를 다음과 같이 수정할 수 있습니다.

func setupClosure() {
    self.presented.closure = { [weak self] in
        self?.printer()
    }
}

Timer

타이머는 프로퍼티에 저장하지 않더라도 이슈가 생길 수 있습니다.

func leakyTimer() {
    let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
        let currentColor = self.view.backgroundColor
        self.view.backgroundColor = currentColor == .red ? .blue : .red
    }
    timer.tolerance = 0.1
    RunLoop.current.add(timer, forMode: RunLoop.Mode.common)
}

 

  1. 타이머는 반복됨
  2. 클로저 내에서 [weak self] 가 사용되지 않고 self 가 참조되고 있음

 

위 2개 조건이 충족되는 한 타이머는 참조된 컨트롤러 혹은 객체가 할당 해제되는 것을 불가능하게 할것입니다. 이 경우는 메모리 누수보다는 Delayed Deallocation 의 경우에 더 가깝습니다.

 

참조된 객체를 계속 살아있는 상태로 유지하는것을 방지하기 위해 [weak self] 를 사용해 어떤 객체도 타이머 내부에서 강한 참조를 갖고있지 않도록 해야하며, 타이머가 필요 없어지는 시점에는 타이머를 Invalidate 해주어야 합니다.

weak self 의 대안

self 를 클로저에 직접 전달하지 않기 위해서 [weak self] 를 쓰는 대신에, self 내에 접근하고 싶은 프로퍼티에 대한 참조를 생성하고, 그 참조를 클로저에 전달하는 방식을 사용할 수 있습니다.

 

클로저 내부에서 view 프로퍼티에 접근한다고 가정해보겠습니다.

func setupAnimator() {
    let view = self.view // view 프로퍼티에 대한 참조 생성
    let anim = UIViewPropertyAnimator(duration: 2.0, curve: .linear) {
        view?.backgroundColor = .red // self 대신 사용
    }
    anim.addCompletion { _ in
        view?.backgroundColor = .white // self 대신 사용
    }
    self.animationStorage = anim
}

위 코드에서 view 프로퍼티에 대한 참조를 생성하고, self 대신 사용하고 있는 모습을 확인할 수 있습니다. 마지막에 anim 변수는 self 의 프로퍼티에 저장되고 아무런 순환참조도 생기지 않습니다.

 

만약 클로저에서 self 의 여러 프로퍼티를 참조하려고 하는 경우, Tuple 을 이용해 컨텍스트를 생성할 수 있습니다.

 

해당 컨텍스트를 클로저에 전달하는 방식으로 self 를 직접 건네주는 것을 대신할 수 있습니다.

func setupAnimator() {
    let context = (view: self.view,
                   navigationItem: self.navigationItem,
                   parent: self.parent)

    let anim = UIViewPropertyAnimator(duration: 2.0, curve: .linear) {
        context.view?.backgroundColor = .red
        context.navigationItem.rightBarButtonItems?.removeAll()
        context.parent?.view.backgroundColor = .blue
    }
    self.animationStorage = anim
}

요약

  • [unowned self] 는 그다지 좋은 방법이 아니다..!
  • Non-Escaping 클로저의 경우, Delayed Deallocation 을 고려하지 않는 한 [weak self] 를 사용하지 않아도 된다
  • Escaping 클로저가 내부에서 어떤 객체를 참조하는 경우, 어딘가 저장되거나, 다른 클로저에 저장된다면 [weak self] 를 사용해라
  • guard let self = self 와 같은 구문은 Delayed Deallocation 을 유발하는 경우가 있고, 상황에 맞춰 self?. 와 같은 Optional Chaining 과 비교하여 잘 사용해야 한다
  • GCD 작업은 어딘가 저장하거나 나중에 실행하지 않는 이상 [weak self] 를 사용하지 않아도 된다
  • Timer 사용 시 유의하자
  • 확실하지 않은 상황에서는 deinit 과 Instruments 를 활용하자

 

정말 긴 글이었습니다..! 작은 의문으로 시작해서 이렇게까지 커질줄은 몰랐네요.

 

이렇게 개념만 학습해서는 금방 잊어버릴수도 있으니 다음에 진행하는 프로젝트에서는 이번 글에서 다룬 내용들에 각별히 유의하여 코드를 작성해봐야겠다는 생각이 듭니다.

 

잘못된 내용이나 지적할 부분은 댓글로 남겨주세요! 감사합니다.