[RxSwift] Disposable과 DisposableBag

이정훈·2024년 3월 31일
0

ReactiveX

목록 보기
5/5
post-thumbnail

🧐 Disposable이란?

Rx에서는 Observable이라고 하는 데이터(정확히 말하면 공식 문서에서는 item이라는 단어를 사용합니다만..) 방출이 가능한 스트림subscribe()라는 메서드를 사용하여 구독이라는 일련의 과정을 통해 Observable이 방출하는 데이터를 받아 볼 수 있었다.

RxSwift에서 subscribe() 메서드의 선언 부분을 살펴보면 아래와 같은데..

public func subscribe(
        onNext: ((Element) -> Void)? = nil,
        onError: ((Swift.Error) -> Void)? = nil,
        onCompleted: (() -> Void)? = nil,
        onDisposed: (() -> Void)? = nil
    ) -> Disposable

메서드의 반환 타입을 보면 Disposable 타입인 것을 알 수 있다. 과연 저 Disposable은 어디에 쓰이는 것일까? 🧐

Disposable이란 '처분 가능한' 정도로 해석할 수 있으며 필자가 생각한 Disposable 타입이 가지는 의미는 dispose()를 호출할 수 있는 타입 정도로 생각한다.

dispose() 메서드

Disposable에 대해 알아보기 앞서 지금까지 알아본 Observable을 살펴보면..

import RxSwift

let observable = Observable<Int>.just(1)

observable.subscribe(
	onNext: { item in
    	print(item)
	}, onCompleted: {
    	print("onCompleted")
	}, onDisposed: {
        print("disposed")
    })

//1
//onCompleted
//disposed

위의 코드에서 just() 메서드는 단 하나의 값을 방출하고 종료되는 Observable을 생성하는 연산자이다. 따라서 Observable1을 방출한 뒤에 더 이상 방출할 항목이 없기 때문에 자동으로 dispose 되어 구독을 해제한다.

dispose란 '처분하다'라는 의미 정도로 해석할 수 있고, Rx에서는 Observable구독 행위를 제거하는 의미로 사용할 수 있다.

  public func subscribe(
        onNext: ((Element) -> Void)? = nil,
        onError: ((Swift.Error) -> Void)? = nil,
        onCompleted: (() -> Void)? = nil,
        onDisposed: (() -> Void)? = nil
    ) -> Disposable {
            let disposable: Disposable
            
            ...
            
            let observer = AnonymousObserver<Element> { event in
            	...
                
                switch event {
                case .next(let value):
                    onNext?(value)
                case .error(let error):
                    if let onError = onError {
                        onError(error)
                    }
                    else {
                        Hooks.defaultErrorHandler(callStack, error)
                    }
                    disposable.dispose()    //dispose() 호출
                case .completed:
                    onCompleted?()
                    disposable.dispose()    //dispose() 호출
                }
            }

RxSwift에서 subscribe() 메서드의 구현 부분을 살펴보면.. 하단의 switch 구문에서 .error 또는 .completed가 발생 하였을때 dispose() 메서드를 호출하는 것을 알 수 있었다.

이번에는 하나의 값을 방출하는 Observable이 아닌 특정 시간마다 특정 값을 방출하는 Observable에 대해서도 살펴보자.

import RxSwift

let observable = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
let disposable = observable.subscribe(onNext: { item in
    print(item)
})

DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
    disposable.dispose()    //5초 뒤에 dispose() 호출
})

interval() 메서드는 지정한 시간 마다 0부터 1씩 증가하는 값을 방출하는 Observable을 생성하는 연산자이다.

위 코드에서 Observable은 초 단위를 1초로 설정했기 때문에 매초마다 무한히 값을 발행한다. 따라서 해당 Observable은 사용자가 직접 dispose() 메서드를 호출하지 않는 이상 프로그램이 강제 종료 되기 전까지는 값을 무한히 방출할 것이기 때문에 위의 코드에서는 프로그램 실행 후 5초 뒤에 dispose() 메서드를 호출 하도록 하였다.

이처럼 더 이상 Observable구독이 필요하지 않다고 판단될 때 dispose() 메서드를 적절히 호출하여 구독취소하는 과정은 매우 중요하다고 할 수 있다.

그렇지 않으면 어디선가 계속 값을 방출하면서 메모리 누수를 발생시키고 있을지 모르니까...

🗑️ Disposable & DisposeBag

실제 프로젝트에서는 여러 개의 Observable이 복합적으로 동작할 수 있는데 각 Observable를 구독하는 Disposabledispose 시점을 생각하여 dispose() 메서드를 실행하는 것이 여간 귀찮은 일이 아닐 수 없다.

이런 불편함을 해소할 수 있는 것이 바로 DisposeBag이다.

DisposeBag, 말 그대로 Disposable 타입을 담을 수 있는 가방이라고 생각하면 된다.

DisposeBag을 통해 어떻게 Observable구독 취소할 수 있냐면, Disposable 타입의 객체를 DisposeBag에 등록해 두면 DisposeBag이 메모리에서 해제될때 등록된 모든 Disposable 객체에 대해 dispose() 메서드를 호출 시켜준다.

아래의 DisposeBag 구현부를 통해 자세히 알아보면...

public final class DisposeBag: DisposeBase {
    
    ...
    
    // state
    private var disposables = [Disposable]()
    private var isDisposed = false
    
    ...

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

        for disposable in oldDisposables {
            disposable.dispose()    //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()
    }
}

deinit의 구현부를 보면 DisposeBagdispose()를 호출하고 있고 이 dispose()는 등록된 disposables에 대하여 for문을 돌면서 Disposable 객체의 dispose() 메서드를 호출하는 것을 확인할 수 있었다.

아래는dispose()를 직접 호출하는 것이 아닌 DisposeBag을 이용하여 메모리 관리를 구현한 예시이다.

import RxSwift

struct SomeStruct {
    private let observable = Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
    private var disposeBag = DisposeBag()
    
    func subscribe() {
        observable.subscribe(onNext: { item in
            print(item)
        })
        .disposed(by: disposeBag)
    }
    
}

var instance: SomeStruct? = SomeStruct()
instance?.subscribe()

DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
    instance = nil
})

//0
//1
//2
//3
//4

Disposable 객체에 disposed(by:) 메서드를 통해 disposeBag를 등록해주면 나중에 instance가 메모리에서 해제될때 disposeBag도 함께 메모리에서 해제 되면서 Observable의 구독을 취소하여 메모리 누수를 방지할 수 있었다.

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글