[RxSwift] Dispose, DisposeBag

강대훈·2025년 1월 5일

RxSwift

목록 보기
3/5
post-thumbnail

Dispose

만약 내가 구독을 해제하고 싶다면 어떻게 해야할까? 구독을 하는 코드를 다시 한 번 확인해보자.

mainButton.rx.tap.subscribe(onNext : { event in
    print("버튼이 클릭되었다!!")
    print(event)
})

간단하게 mainButton 이 눌렸을 때의 상태를 관찰하고 이벤트를 받는 코드다. 여기서 사실 subscribe 함수는 반환값을 가지고 있다.

  • subscribe 함수의 원형
public func subscribe(onNext: ...) -> Disposable
// Disposable -> 없앨 수 있는

Disposable 이라는 타입을 반환하고 있는데, Disposable 은 번역해보면 없앨 수 있는 이라는 단어의 뜻을 가지고 있다. 즉, 구독을 취소할 수 있는 객체를 반환한다는 것이다.

그러면 Disposable 반환값을 받아보자.

let disposable = mainButton.rx.tap.subscribe(onNext : { event in
    print("버튼이 클릭되었다!!")
    print(event)
})

disposable.dispose() // 구독 취소!

객체를 반환받고 dispose() 메소드를 통해서 구독을 취소하는 코드다. dispose() 메소드는 Disposable 객체에 내장되어 있는 메소드인데 dispose() 가 실행된 순간부터 더이상 mainButton 을 누르더라도 이벤트는 발생하지 않는다. 즉 구독이 취소된 것이다.

public protocol Disposable {
    /// Dispose resource.
    func dispose()
}

다음과 같이 Disposable 에는 dispose() 메소드를 가지고 있는 것을 확인할 수 있다! dispose() 메소드는 메모리 낭비와 관련이 있기 때문에 잘 사용해줄 필요가 있다.

하지만 모든 Disposable 객체에 dispose() 메소드를 작성해주기엔 매우 번거로운 일이고 메모리가 해제되는 시점에 모두 작성해주는 것은 굉장히 어려운 일이다.

이에 따라서 편리하게 구독의 취소를 관리해주는 DisposeBag 이라는 객체가 등장한다.

DisposeBag

Disposable 객체들을 담는 배열

먼저 이해를 돕기 위해서 아래의 코드를 보자.

var disposeBag : DisposeBag = .init()

mainButton.rx.tap.subscribe(onNext : { event in
    print("버튼이 클릭되었다!!")
    print(event)
}).disposed(by : disposeBag)

DisposeBag 을 만드는 법은 매우 간단하다. DisposeBag() 을 사용하거나 타입을 명시해준뒤에 초기화 해주면 된다.

Disposable 에는 disposed(by bag : DisposeBag) 이라는 메소드가 내장되어 있다. 이 메소드를 통해서 쉽게 Disposable 객체들을 DisposeBag 에 넣어줄 수 있다.

extension Disposable {
    /// Adds `self` to `bag`
    ///
    /// - parameter bag: `DisposeBag` to add `self` to.
    public func disposed(by bag: DisposeBag) {
        bag.insert(self)
    }
}

어떻게 이렇게 간단한 작업만으로도 구독을 취소하여 메모리 낭비를 방지할 수 있을까?

그 이유는, DisposeBag 의 인스턴스가 메모리에서 사라질 때 (deinit), 자기 자신에게 dispose() 를 호출해서 구독을 모두 취소하고 메모리 낭비를 막아주기 때문이다.

  • DisposeBag 의 구독 취소 방법
public final class DisposeBag: DisposeBase {
    deinit {
        self.dispose()
    }
} 

DisposeBag 메모리 누수?

그렇다고 해서 DisposeBag 은 과연 무조건 메모리 낭비를 막아주는 만능 객체일까?

위의 코드를 보면 deinit 되는 시점에서 DisposeBag 에 들어있는 구독을 dispose() 하는 것을 볼 수 있다.

반대로 바꿔 말한다면, DisposeBag이 메모리에서 사라지지 않는다면 (deinit 되지 않는다면) DisposeBag에 들어있는 모든 구독은 계속 유지되어서 메모리 낭비로 이어진다는 것이다.

DisposeBag 이 메모리에서 사라지는 경우는 어떤 일이 있을까? 가장 흔하게 접할 수 있는 방법은 바로 ViewController 가 메모리에서 해제되는 순간일 것이다.

하지만 메모리가 해제되지 않는다면 어떻게 될까? 아래 코드를 보자.

class ViewController2: UIViewController {
    
    var count : Int = 0
    var disposeBag : DisposeBag = .init()
    
    let rxButton : UIButton = {
    	// Button Code
    }()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let backButton = UIBarButtonItem(barButtonSystemItem: .rewind,
                                         target: self,
                                         action: #selector(onBack))
        navigationItem.setLeftBarButton(backButton, animated: true)
        
        rxButton.rx.tap.asObservable().subscribe(onNext : { _ in
            self?.count += 1
        }).disposed(by: disposeBag)
    
        print("ViewController1 - viewDidLoad (+1)")
    }
    
    @objc func onBack() {
        print("ViewController1 - onBack (+1)")

        DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
            self.view.backgroundColor = UIColor.red
            print("ViewController1 - after 5 secs. (-1)")
        })

        self.navigationController?.popViewController(animated: true)
    }

    deinit {
        print("ViewController1 - deinit (-1)")
    }
}

위의 코드에서 과연 disposeBag 은 과연 메모리 누수를 막을 수 있을까?

정답은 메모리 누수가 발생한다는 것이다.

어떻게 메모리 누수가 발생하는 것일까? 실행 흐름을 한 번 보자.

navigationItem 을 클릭할 때, onBack() 의 메소드가 실행된다.

onBack() 메소드로 인해서 현재 뷰컨트롤러가 pop 되어서 화면이 사라진다. 하지만 비동기 함수가 self 를 강하게 참조하고 있기 때문에 5초 정도 이후에 뷰컨트롤러의 메모리가 해제 되는 것을 기대할 수 있다.

하지만 뷰컨트롤러의 메모리는 해제 되지 않는다. 그 이유는 바로,

rxButton.rx.tap.asObservable().subscribe(onNext : { _ in
            self?.count += 1
        }).disposed(by: disposeBag)

구독 과정에서 self를 강하게 참조하고 있기 때문이다.

rxButton 을 구독하는 스트림은 이벤트를 한 번 방출하고 끝나는 것이 아니라 몇 번을 눌러도 이벤트를 계속 방출한다. 그렇기 때문에 해당 클로저는 지속적으로 self를 강하게 참조하고 있기 때문에 뷰컨트롤러의 메모리는 해제되지 않는다.

그렇기 때문에 구독을 끊고 싶다면 다음과 같은 방법을 사용해야 한다.

  • weak self 를 통한 약한 참조 사용
rxButton.rx.tap.asObservable().subscribe(onNext : { [weak self] _ in
	self?.count += 1
}).disposed(by: disposeBag)
  • DisposeBag 직접 초기화
@objc func onBack() {
    print("ViewController1 - onBack (+1)")

    DispatchQueue.main.asyncAfter(deadline: .now() + 3, execute: {
        self.view.backgroundColor = UIColor.red
        print("ViewController1 - after 5 secs. (-1)")
    })
	self.disposeBag = DisposeBag() // 구독 초기화
    self.navigationController?.popViewController(animated: true)
}

무조건 DisposeBag 을 사용한다고 메모리 누수를 막는 것이 아니기 때문에, 다음과 같은 상황들을 생각하고 사용해 볼 필요가 있음을 유의하자!

(메모리 누수의 예제 및 실험은 곰튀김님의 예제를 통해 학습을 진행했습니다🙂)

참고자료

https://iamchiwon.github.io/2018/08/13/closure-mem/

0개의 댓글