이번 시간에는 dispose에 대해 알아보자. dispose를 번역하면 말 그대로 '처분하다, 처리하다'라는 뜻을 가지고 있다.
Dispose의 개념을 알아보고 이것은 단점을 보완하기 위해 DisposeBag 의 활용까지 알아보자.
다음 코드 예시를 살펴보면서 개념을 익혀보자.
disposable = downloadJSON(downloadURL)
.map { json in json?.count ?? 0 } // operator
.filter { count in count > 0 } // operator
.map { "\($0)" } // operator
.observe(on: MainScheduler.instance) // sugar api - operator
.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
.subscribe(onNext: { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
})
만약 비동기로 작업되는 예를 들어 큰 용량의 이미지를 받고 있는데 유저가 화면을 뒤로 이동하거나 했을 때 이 작업을 취소할 필요가 있다.
그럴 때 다운로드 작업을 백그라운드에서 실행시킬 것이 아니라면 작업을 취소할 필요가 있다. disposable 을 dispose 해주는 작업이 필요하다.
Disposable, dispose 이것들이 뭘까?
그래서 어떻게 취소할까? 라는 의문이 생긴다. Observable의 이벤트를 Observer가 받아 구독했는데 더 이상 이벤트를 받고 싶지 않아 구독 해제하고 싶을 때 구독을 해제하고 리소스를 정리할 수 있도록 도와주는 것이 disposable이다.
그럼 다음 예시를 살펴보면서 좀 더 자세히 알아보자.
해당 클래스에서 disposable 이라는 멤버 변수를 하나 가지고 있고 해당 Observable 에서 나온 return 값을 disposable 에 할당한다.
final class ViewController: UIViewController {
@IBOutlet var timerLabel: UILabel!
@IBOutlet var editView: UITextView!
var disposable: Disposable?
}
위의 다운로드 받는 Observable의 예시 코드의 return 값을 살펴보면 disposable이다.
return 값이 disposable인 것을 확인했고 근데 이것 가지고 뭘 어떻게 한다는 걸까? 하지만 disposable이 어떤 타입인지 내부 구조를 살펴보면 답이 조금씩 보일 것 같다.disposable이 무엇인지 조금 파헤쳐보자.
Disposable은 protocol로 이루어져 있고 dispose라는 함수를 요구사항으로 정의하고 있다. 그럼 또 dispose가 궁금해지는데 내부 코드를 살펴보면 해당 Disposable 리소스를 전부 정리해주도록 구현되어있다.
그럼 다시 돌아가서 위의 작업에서 사용자가 화면을 나갔을 때 다운로드를 중단하는 코드를 작성해야한다고 했다.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
disposable?.dispose()
}
그렇다면 이런식으로 뷰컨트롤러의 생명주기를 활용해 view가 disappear될 때 해당 작업의 return 값은 disposable을 직접 dispose 메서드를 호출해서 정리해주면 된다. 그렇다 dispose가 바로 observable 이벤트의 구독을 해제하고 리소스를 정리하는 메서드이다.
간단하게 disposable과 dispose의 개념을 알아봤고 어떻게 효율적으로 사용할 수 있을지 확인해보자.
하지만 이런방식의 단점은 여러 Observable을 한번에 정리하기 힘들다는 것이다. 각각의 작업마다 disposable 변수를 생성해 각각의 매칭되는 disposable을 dispose하는 작업이 필요할 것이다. 그럼 이것을 또 한 단계 나아가 배열로 해결해볼 수 있을 것 같다. disposable을 배열로 가지고 있고 배열에 해당 observable 작업들의 disposable을 append한 뒤 마지막에 배열 전체를 dispose하는 방법으로 사용할 수 있다.
disposable 배열을 활용해서 한번에 dispose하기
var disposable = [Disposable]()
우선 disposable을 담을 수 있는 배열을 하나 선언 해준다.
let disposable = downloadJSON(downloadURL)
.map { json in json?.count ?? 0 } // operator
.filter { count in count > 0 } // operator
.map { "\($0)" } // operator
.observe(on: MainScheduler.instance) // sugar api - operator
.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
.subscribe(onNext: { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
})
disposable.append(disposable)
우선 dipose해줄 작업들을 배열에 추가한 뒤 똑같이 viewWillDisappear을 통해 배열을 전체적으로 한번에 dispose해주면 된다.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
disposable.forEach {$0.dispose()}
}
그런데 이렇게 하는 방법 또한 귀찮을 수 있다. 그래서 Rx에서는 이를 조금 더 쉽게 사용하기 위해 sugar api를 제공한다. 그것이 바로 disposeBag이다. 어떻게 사용하는지 한번 알아보자.
disposeBag 알아보기
- disposeBag 인스턴스 생성
final class ViewController: UIViewController {
@IBOutlet var timerLabel: UILabel!
@IBOutlet var editView: UITextView!
var disposaBag: disposaBag()
}
- disposaBag에 추가
let disposable = downloadJSON(downloadURL)
.map { json in json?.count ?? 0 } // operator
.filter { count in count > 0 } // operator
.map { "\($0)" } // operator
.observe(on: MainScheduler.instance) // sugar api - operator
.subscribe(on: ConcurrentDispatchQueueScheduler(qos: .default))
.subscribe(onNext: { json in
self.editView.text = json
self.setVisibleWithAnimation(self.activityIndicator, false)
})
disposaBag.insert(disposable)
하지만 insert 또한 굳이 변수에 넣고 또 insert하는 작업이 귀찮기 때문에 해당 Observable 작업에 바로 dispose를 넣을 수 있다.
만약 구독 받은 observable들이 여러개라면 조금 귀찮을 수 있겠네요. 이것을 쉽게 해결해주기 위해 disposable의 extension을 살펴보면 다음과 같은 메서드가 추가적으로 구현되어 있는 것을 볼 수 있다.
disposed(by bag: DisposeBag) 이라는 메서드를 사용해보자. 파라미터에 DisposeBag 객체가 들어가고 그 bag에 disposable(자신, self)를 insert하는 방식이다. 그러면 disposeBag 객체를 선언해둔 class 가 deinit될 때 같이 소멸될 것이고 등록된 모든 disposable이 다 같이 dispose 되도록 구현되어있는 메서드이다.
그렇다면 정말로 해당 뷰컨트롤러 인스턴스가 소멸되면 disposeBag 객체도 소멸되면서 observable이 dipose되는지 까지 확인해보자.
class ViewController: UIViewController {
var disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
bind()
}
deinit {
print("deinit ViewController")
}
func bind() {
Observable<Int>.interval(.seconds(1), scheduler: MainScheduler.instance)
.take(10)
.subscribe(onNext: { value in
print(value)
}, onError: { error in
print(error)
}, onCompleted: {
print("onCompleted")
}, onDisposed: {
print("onDisposed")
})
.disposed(by: disposeBag)
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
UIApplication.shared.keyWindow?.rootViewController = nil
}
}
}
/*
0
1
2
deinit ViewController
onDisposed
*/
예시의 뷰컨트롤러는 rootViewController로 지정되어 있는 상태이다.
Observable을 생성하고 해당 메서드가 실행되면 3초뒤에 rootViewController에 nil을 할당해서 소멸시킨다.
이렇게 Observable 작업에 disposeBag 을 선언해주면 viewWillDisapper같은 곳에 따로 dispose작업을 하지 않아도 해당 viewController가 deinit(소멸) 될때 자동으로 dispose되면서 작업이 취소가 된다.
🔥 하지만 여기서 주의점은 viewController가 항상 deinit되는지 확인하고 메모리 누수가 일어나고 있지는 않은지 확인을 제대로 해야 dispose가 작동되기 때문에 유의하여 사용해야한다.
RootViewController에서의 dipose 문제
근데 잘 생각해보면 rootViewController에서 인스턴스를 소멸시키는 위의 예제는 예제일뿐 실제 앱을 만들 때 문제점이 생길 수 있다.
위의 예시처럼 계속 이벤트가 동작하는 타이머 같은 이벤트를 rootViewController에서 작업한다면 앱의 첫화면은 deinit 되지 않고 무한히 작동하게 되어 메모리가 낭비 될 수 있다. (물론 위의 예제는 take가 있기 때문에 무한히 동작하지는 않는다.)
이럴 때는 rootViewController에서 임의로 해당 작업을 dispose 시켜줄 필요가 있다.
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
// 해당 asyncAfter 스코프가 끝나면 DisposeBag() 인스턴스가 deinit될테니 여기서 리소스 정리가 됨
self.disposeBag = DisposeBag()
}
이렇게 로직 상 적당한 시점에 리소스가 잘 정리 될 수 있도록 해야하는 작업이 필요할 것이다.
지금까지 disposable, dispose, disposeBag의 개념까지 알아봤다. 그냥 큰 맥락에서 보면 해당 이벤트의 구독을 끊고 리소스를 정리하는 개념이다. 그렇게 어렵지는 않지만 한번 시간내에 자세히 알아두고 학습해두면 RxSwift를 계속 학습할 때 도움이 될 것 같다.
'iOS > RxSwift' 카테고리의 다른 글
RxSwift - API Request, Error Handling (+ Button Tap Stream), Single Traits (0) | 2023.11.16 |
---|---|
RxSwift - Unicast, Multicast (1) | 2023.11.06 |
RxSwift - Subject (1) | 2023.11.02 |
RxSwift 알아보기 (1) | 2023.11.01 |