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()
를 호출할 수 있는 타입 정도로 생각한다.
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을 생성하는 연산자이다. 따라서 Observable은 1
을 방출한 뒤에 더 이상 방출할 항목이 없기 때문에 자동으로 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()
메서드를 적절히 호출하여 구독을 취소하는 과정은 매우 중요하다고 할 수 있다.
그렇지 않으면 어디선가 계속 값을 방출하면서 메모리 누수를 발생시키고 있을지 모르니까...
실제 프로젝트에서는 여러 개의 Observable이 복합적으로 동작할 수 있는데 각 Observable를 구독하는 Disposable의 dispose 시점을 생각하여 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
의 구현부를 보면 DisposeBag
의 dispose()
를 호출하고 있고 이 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의 구독을 취소하여 메모리 누수를 방지할 수 있었다.