[RxSwift] 순환참조는 왜 일어날까?

jane·2022년 9월 24일
0

RxSwift

목록 보기
3/4
post-thumbnail

RxSwift의 Operator을 사용할때 순환 참조가 일어나지 않도록 클로저의 캡쳐리스트에 [weak self]로 약한 참조를 해야한다.

어떻게 순환 참조가 일어난다는건지 항상 궁금해서 찾아봤지만 이해가 안되다가 이제 드디어 이해가 되어서 이 유레카 모먼트를 기록해놓는다. 😇

사전 개념

클로저가 Context를 Capture하는 속성때문에
escaping closure내에서 클래스의 인스턴스를 참조하면, 강한 순환 참조가 발생할 수 있다.

  • non-escaping closure인 경우에는 함수가 종료될때 해당 함수의 scope를 벗어나지 않아 클로저도 메모리에서 해제되어 강한 참조를 해도 상관없는데,
func someFunction(completion: () -> Void = {}) {
  completion()
  print("someFunc!")
  return
}
  • escaping closure는 함수가 종료되어도 해당 함수의 scope를 벗어나 함수 종료 후에 실행되기 때문에 강한 순환 참조가 발생할 수 있는 것이다.
func someFunction(completion: @escpaing () -> Void = {}) {
  self.someDelayProcess {
    completion()
  }
  print("someFunc!")
  return
}

이렇게 escaping closure가 강한 순환 참조의 가능성을 가지고있는데... RxSwift 의 operator들과 subscription 메서드들은 모두 escaping closure로 이루어져 있다. 😱

따라서 저런 escaping closure를 파라미터로 받는 메서드들을 사용할때 꼭! 잊지말고 해줘야하는게 약한참조다

RxSwift에서 순환참조를 해결하기 위해 weak self를 사용한다

RxSwift로 비동기 처리를 할때 이런 코드를 많이 봤을 것이다.

final class MyViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private let parser = MyModelParser()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let parsedObject = theObservable
            .map { [weak self] json in
                return self.parser.parse(json)
            }
            
        parsedObject.subscribe(onNext:{ _ in 
            //do something
        })
        .disposed(by: disposeBag)
    }
}

보통 ViewController에서 Observable의 subscription이 일어나기 때문에 이런식으로 코드를 작성한다.
DisposeBag를 이용해서 subscribe의 리턴값인 Disposable을 저장하고, 클로저의 캡쳐리스트에 [weak self]로 self를 약하게 참조한다.

순환참조를 해결하기 위해 약하게 참조하는건 알겠는데... 어떤 부분에서 순환참조가 생긴다는건지 궁금했다.

아무리 봐도 ViewController는 Observable을 참조하고 있지 않은데, 어디서 순환참조가 생긴다는거지?

범인은 DisposeBag이었다!

DisposeBag을 사용하는 이유

간단히 말하자면 Observable과 Disposable의 강한 순환참조를 해결하기 위해서 Disposable마다 dispose()를 해줘야하는데, 일일히 해주기 귀찮아서 DisposeBag을 사용한다.

Observable을 subscribe하면, Rx는 Observable과 Disposable간에 강한 순환 참조를 만든다.
Observable <-> Disposable
따라서 ViewController가 화면에서 사라져서 deinit이 되더라도, 둘 간의 강한 순환 참조 때문에 subscription이 취소되지 않는다.

따라서, 이를 해결하기 위해서 Disposable에는 구독을 취소하는 dispose() 메서드가 있다.

ViewController의 deinit시점에 아래처럼 dispose()한다면 Disposable이 deinit되면서 Observable도 deinit된다~!

	final class MyViewController: UIViewController {
    var subscription: Disposable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        subscription = theObservable().subscribe(onNext: {
            // handle your subscription
        })
    }
    
    deinit {
        subscription?.dispose()
    }
}

하지만... ViewController에서 subscription이 한번만 일어나지 않고 여러번 일어난다면, 매 구독마다 저렇게 일일히 dispose를 시켜주는 것은 너무나도 귀찮은 일이기때문에 DisposeBag의 개념이 나온다.

DisposeBag를 ViewController의 프로퍼티로 두고, 구독이 일어날때마다 DisposeBag에 Disposable을 차곡차곡 넣어놓으면, ViewController의 deinit 시점에 DisposBag의 deinit이 불리면서 자기가 가지고 있던 모든 Disposable을 dispose() 시킨다.

아래 DisposeBag의 구현 부분을 보면,, deinit 시점에 정말 자기가 가진 모든 Disposable을 차례로 dispose() 시키는 모습을 볼 수 있다.

public final class DisposeBag: DisposeBase {
    
    private var lock = SpinLock()
    
    // state
    private var disposables = [Disposable]()
    private var isDisposed = false
    
    /// Constructs new empty dispose bag.
    public override init() {
        super.init()
    }

    /// Adds `disposable` to be disposed when dispose bag is being deinited.
    ///
    /// - parameter disposable: Disposable to add.
    public func insert(_ disposable: Disposable) {
        self._insert(disposable)?.dispose()
    }
    
    private func _insert(_ disposable: Disposable) -> Disposable? {
        self.lock.performLocked {
            if self.isDisposed {
                return disposable
            }

            self.disposables.append(disposable)

            return nil
        }
    }

    /// This is internal on purpose, take a look at `CompositeDisposable` instead.
    private func dispose() {
        let oldDisposables = self._dispose()

        for disposable in oldDisposables {
            disposable.dispose()
        }
    }

    private func _dispose() -> [Disposable] {
        self.lock.performLocked {
            let disposables = self.disposables
            
            self.disposables.removeAll(keepingCapacity: false)
            self.isDisposed = true
            
            return disposables
        }
    }
    
    deinit {
        self.dispose()
    }
}

그런데, DisposeBag을 사용했을 때 생기는 문제가 있다.
ViewController가 DisposeBag을 참조하게 되면서...
ViewController -> DisposeBag

DisposeBag 안에는 Disposable이 들어있으니...
DisposeBag -> Disposable

이런 끔찍한 사각관계가 형성이 되어버렸다 ...

이렇게 되면 아래 코드에서 map의 escaping closure에서
self(aka. ViewController)를 강하게 참조하고 있어서 참조 카운트가 1이 되어 ViewController가 화면에서 내려가도 deinit이 안불린다.

final class MyViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private let parser = MyModelParser()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let parsedObject = theObservable
            .map { json in
                return self.parser.parse(json)
            }
            
        parsedObject.subscribe(onNext:{ _ in 
            //do something
        })
        .disposed(by: disposeBag)
    }
}

그렇게 되면 아까 말했던 ViewController deinit -> DisposeBag deinit -> Disposable deinit -> Observable deinit의 아름다운 deinit이 불가능해진다.

근데 사실 해결책은 간단하다
[weak self] 로 Observable이 ViewController를 약하게 참조하면 되는 것,,, ㅋ

final class MyViewController: UIViewController {
    private let disposeBag = DisposeBag()
    private let parser = MyModelParser()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let parsedObject = theObservable
            .map { [weak self] json in
                return self.parser.parse(json)
            }
            
        parsedObject.subscribe(onNext:{ _ in 
            //do something
        })
        .disposed(by: disposeBag)
    }
}

이렇게되면 ViewController가 화면에서 내려갈때 참조 카운트가 0이되어서 정상적으로 deinit이 불리게 된다 😚

이렇게 어떤식으로 순환참조가 일어나는지, 왜 클로저의 캡쳐리스트에 weak self를 사용해서 순환참조를 해결할 수 있는지 알아보았다 ~!

Reference

Memory management in RxSwift – DisposeBag

profile
제가 나중에 다시 보려고 기록합니다 ✏️

0개의 댓글