RxSwift) Dispose /Disposable / DisposeBag 이해하기
안녕하세요, 소들입니다 XD
hm.. 요즘 제 블로그 댓글의 대부분이
소들님은 개발 공부를 어떻게 하셨나요? 에 대한 내용인듯 한디..
교과서.. 위주로.. 공부하셈.. 두번하셈.. 라고 할뻔... ㅎㅎ
나중에 제 공부 방법에 대해서도 한번 포스팅을 해볼게욥!!
저번 포스팅에서 Observable과 Observer가 무엇이고
Subscribe가 무엇인지에 대해서 공부를 해봤잖아요??!!
이번 포스팅에선 Dispose에 대해 공부해볼 것입니당!!
모든 포스팅은 편의 말투로 합니다~!!
1. Disposable과 dispose()
여러분 Dispose라는 단어의 뜻을 아셈??
Dispose는 "처리하다/없애다"라는 뜻을 갖고 있음!!
그럼 멀 처리하나염..??
자, 관찰 가능한 형태 Observable이 있을 때
Observer는 이 Observable을 "구독"을 통해 이벤트를 받을 수 있다 했음
(좀 더 정확히는 Obsever가 Observable이 방출하는 항목에 대해 받는 것)
그러면.. 내가 만약 더이상 이 Observable에 대한 이벤트를 받고 싶지 않아서..
"구독을 해제"하고 싶을 땐 어떻게 하나요!?
할 때 사용할 수 있는 것이 바로 Disposable이라는 것임!
아항,,,!! 머라능겨,,!!
여러분 이전 포스팅에서 우린 Subscribe라는 메서드를 통해
Observer가 Observable을 구독할 수 있단 것을 알게 되었잖음!?
근데 Subscribe 메서드 원형을 다시 살펴보면
이렇게 반환 타입이 Disposable인 것을 볼 수 있음!!
즉, Subscribe 메서드를 호출할 경우 Observable을 구독 할 수 있지만
해당 Observable의 이벤트가 나가리라 더이상 필요하지 않을 때
해당 구독을 취소할 수 있는 수단인 Disposable이란 타입의 값을 리턴하여 준단 말임!
자, 그럼 리턴 타입인 Disposable을 살펴보면
이렇게 dispose라는 메서드를 가진 Protocol로 정의되어 있음!!
여기까지 봤을 때 유추해볼 수 있는 것은,
Observer는 Observable을 구독하기 위해 Subscribe라는 메서드를 사용하는데,
이 Subscribe 메서드는 구독을 해제할 때 사용하는 Disposable이란 타입의 인스턴스를 반환하는구나!
그러면 이 리턴된 Disposable타입의 값에 대고 dispose() 메서드를 호출하면!!
우리가 구독한 Observable에 대해 구독을 해제할 수 있겠구나!
정답입니다~~!
예제로 보자
let sodeulDisposable = sodeulButton.rx
.tap
.subscribe(onNext: {
print("소들이 버튼 눌림!")
})
sodeulDieButton.rx
.tap
.subscribe(onNext: {
sodeulDisposable.dispose()
print("소들이 버튼을 구독 해제하자!")
})
|
위 코드를 보면,
sodeulButton의 tap 이벤트에 대해 subscribe를 통해 구독을 하고,
해당 구독을 취소할 수 있는 리턴 값,
즉 Disposable 인스턴스를 sodeulDisposable이란 변수에 저장 했음!!!
그리고 sodeulDieButton이 눌렸을 때
이 sodeulDisposable의 dispose() 메서드를 호출 해보는 것임!!!
그럼 어떻게 되냐
sodeulDieButton이 눌리기 전까지 잘 동작하던 sodeulButton의 tap 이벤트가,
sodeulDieButton이 눌리는 순간
sodeulButton의 tap Observable에 대한 구독을 해제해주기 때문에,
더이상 sodeulButton의 tap 이벤트에 대한 처리를 할 수가 없어짐!!!
따라서 구독 해제 후에 버튼을 누르면 더이상 아무런 이벤트를 하지 않는 것
정리하자면,
Observer가 어떤 Observable의 이벤트를 받고 싶을 경우,
Subscribe 메서드로 "구독"을 통해 할 수 있었음!
근데 더이상 이벤트를 받고 싶지 않아서 "구독을 해제"할 경우를 대비해
Subscribe 메서드는 Disposable이란 타입의 리턴 값을 줌
이 Disposable은 프로토콜 타입으로 dispose 메서드를 가지고 있는데,
만약 Observable이 방출하는 이벤트를 더이상 받고 싶지 않은 경우,
Subscribe를 했을 때 얻은 이 Disposable에 대고 dispose() 메서드를 호출할 경우
해당 Observable에 대한 구독을 해제 할 수 있음
이해가.. 됐.. 겠지..?
근데 사실 dispose 메서드를 실행하는 건,
해당 시점(내가 원하는 시점)에 해당 이벤트의 구독을 끊기 위함인 것도 있지만
가장 중요한 이유 중 하나는 바로
메모리 관리
를 위해서임!!
Observable은 기본적으로,
complete이나 error가 발생하기 전까진 계속 이벤트를 방출 시키는데,
따라서 이벤트가 더이상 방출되면 안 되는 시점에서 우리는 이 리소스를 직접 deinit 해줘야 함!!
만약 이 deinit 해주는 과정을 하지 않으면,
이 리소스는 계속해서 필요할 때마다 이벤트를 방출시키는 메모리 릭(leak)으로 이어짐
그러면 어떻게 해당 Observable과 관련된 리소스를 deinit 시킬 수 있냐구?
바로 위에서 공부한 dispose() 메서드가 있짜너!!!
Disposable 타입의 인스턴스의 dispose 메서드를 호출한다는 것은
해당 Observable에 대한 리소스를 deinit 시킨다는 것
즉, Rx에서 메모리 관리를 한단 것
라고 볼 수있음!!ㅎㅎ
따라서 Rx에서 이 Dispose는 메모리와 관련이 있기 때문에 아주 중요한 메서드였떤 것임
사실 엥 갑자기 메모리 릭이요?
이벤트가 더이상 방출되지 않는 시점이요? 0.1도 와닿지 않는데요?
할 ㅎ수 있기 때문에 이와 관련된 예제를 보여주겠음
class SodeulViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let _ = Observable<Int>.interval(
.seconds(1),
scheduler: MainScheduler.instance
)
.subscribe(onNext: {
print($0)
})
}
}
|
당장 Observable을 생성하는 Operator는 모를 수 있으니
위코드의 역할을 잠깐 설명하자면
SodeulViewController가 실행되면,
1초에 한번씩 0부터 순차적인 숫자(0,1,2,3...)을 방출하는 Observable을 만들고,
해당 Observable을 구독하여 이벤트가 방출됐을 때, 그 숫자를 출력하게 한 것임
그리고 해당 구독을 통해 리턴받은 Disposable 타입의 인스턴스론 아무짓도 하지 않음
자, 이렇게 될 경우 어떤 문제가 생기는지 보겠음
SodeulViewController가 생성되어 띄어졌을 때,
1초에 한번씩 순차적인 숫자를 방출하는 Observable이 생성되고,
해당 Observable이 이벤트를 방출 했을 때 subscribe 클로저에 의해 0,1,2가 찍히고 있음
여기까진 문제가 되지 않음!!
근데 2까지 찍히고 나서 SodeulViewController를 내려서,
해당 Observer의 이벤트를 실행하던 ViewController가 앱에서 사라졌음에도 불구하고,
해당 Observer에 대한 리소스가 해제되지 않아서
죽지 않고 계속 이벤트를 방출하고 있는 메모리 leak이 발생하고 있는 것임!!
(이 Observable을 생성해서 Disposable을 갖고 있던 ViewController는
이미 deinit 되어버렸기 때문에 해당 리소스를 해제할 수 있는 방법이 없음)
따라서, 다음과 같이
class SodeulViewController: UIViewController {
private var timerDisposable: Disposable?
override func viewDidLoad() {
super.viewDidLoad()
timerDisposable = Observable<Int>.interval(
.seconds(1),
scheduler: MainScheduler.instance
)
.subscribe(onNext: {
print($0)
})
}
deinit {
timerDisposable?.dispose()
}
}
|
나를 갖고 있는 ViewController가 deinit 되는 시점에
해당 Disposable에 대한 dispose를 통해
해당 Observable 리소스에 대해서도 같이 deinit을 실행시켜주면,
위처럼 ViewController가 deinit 될 때
해당 Observable 리소스에 대한 dinit도 같이 이루어지기 때문에
메모리 릭이 나지 않음!!!
이제 이 Disposable과 dispose 메서드에 대해 다 이해를 했길..!!
2. DisposeBag
DisposeBag은 ㅁㅓ냐하면,,,
말 그대로 Disposable을 담는 Bag.. 🎒
자, 위에서 배운 대로 우린 해당
Observable 리소스가 해제되어야 할 시점에 dispose를 호출해주어야 한다고 헀음!
근데 만약 다음과 같이, 해제해줘야 하는 리소스가 많아지면,
class SodeulViewController: UIViewController {
private var timerDisposable: Disposable?
private var button1Disposable: Disposable?
private var button2Disposable: Disposable?
...
deinit {
timerDisposable?.dispose()
button1Disposable?.dispose()
button2Disposable?.dispose()
}
}
|
위처럼... 지저분한 전역변수와 코드가 늘어날 거잖음.......!????
아아 -...
누가 이 Disposable들을 담는 배열 좀 만들어줬으면 좋겠는데 ........
네 만들어드렸습니다~
누가!? DisposaBag이!
RxSwift에서 제공하는 이 DisposeBag이란 것은
쉽게 말해서 Disposable 객체를 담는 배열인 것임!!
class SodeulViewController: UIViewController {
private var disposeBag: DisposeBag = .init()
}
|
위처럼, 전역 변수로 disposeBag 하나를 선언해주고,
disposable 객체를 이제 어떻게 담냐면!!!!
Disposable 프로토콜의 기본 메서드로 제공하는
이 disposed(by bag: DisposeBag) 메서드를 이용해서 담을 것임!!
class SodeulViewController: UIViewController {
private var disposeBag: DisposeBag = .init()
override func viewDidLoad() {
super.viewDidLoad()
Observable<Int>.interval(
.seconds(1),
scheduler: MainScheduler.instance
)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
}
|
위처럼 subscribe를 통해 배출된 Disposable 객체에 대고
disposed(by bag:) 메서드로 미리 선언된 disposeBag 객체를 넘겨주면,
해당 disposeBag 안에 해당 Disposable이 담기게 됨!!!
자, 그럼 이제 어떤 생각이 드냐면,
그럼 deinit되어야 할 때 disposeBag 안에 들어 있는 Disposable 객체들을 다
dispose() 시켜야겠네요!!!???
라는 생각이 들잖음??
근데, 실제로 위 코드처럼 굳이 deinit 때 아무런 작업을 해주지 않아도
메모리 릭이 일어나지 않음!!!!!!1
이는 왜그러냐면,
DisposeBag 객체를 좀맘ㄴ 까보면 되는데
위처럼 DisposeBag 객체 자체가 deinit되는 순간,
내부에 있는 dispose() 메서드를 호출하는데
이 메서드에서 해당 Disposables란 Disposable 배열에 담긴
disposable을 순회하면서 모두 dispose() 메서드를 호출해주기 때문임!!!
(물론 self._dispose 메서드에서 뭐 Lock과 관련된 내용이 있는데.. 이 내용은 생략 하겠음!!)
정리하자면,
DisposeBag을 전역 프로퍼티로 선언해준 경우,
(다른 곳에서 참조하지 않은 경우) 해당 DisposeBag을 담고 있는 인스턴스가 사라질 때
DisposeBag이란 프로퍼티도 같이 deinit이 될 것이고,
이때 내가 갖고 있는 Disposable의 배열을 순회하며 dispose() 메서드를 호출해주기 때문에
딱히 별도의 작업을 해주지 않아도,
DisposeBag이 갖고 있는 Disposable들의 메모리 릭이 나지 않을 수 있던 것!!
(만약 특정 순간에 disposeBag을 직접 비워주어야 하는 상황이면
해당 상황에 맞게 disposeBag을 메모리에서 해제해주면 됨!!)
+ 위에서 말했듯
disposeBag은 자신이 deinit 될 때
자신이 갖고 있던 해당 Disposable을 모두 dispose() 시켜버리기 때문에,
class SodeulViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let disposeBag: DisposeBag = .init()
Observable<Int>.interval(
.seconds(1),
scheduler: MainScheduler.instance
)
.subscribe(onNext: {
print($0)
})
.disposed(by: disposeBag)
}
}
|
위처럼 만약 disposeBag을 전역이 아닌 viewDidLoad 메서드 안의 지역변수로 선언할 경우,
viewDidLoad 메서드가 끝날 때 disposeBag도 같이 메모리에서 해제되기 때문에
이때 등록되어있는 Observable 또한 다 같이 구독 해제 되어 버려 위 코드에서의 Observable이 작동하지 않을 것임
따라서, disposeBag을 사용할 땐 유의 하시길!(보통 전역에 둠)
3. Subscribe 할 때 마지막 클로저 onDisposed: 는 말이죠
내가 이전 Subscribe 메서드를 설명할 떄,
public func subscribe(
onNext: ((Element) -> Void)? = nil,
onError: ((Swift.Error) -> Void)? = nil,
onCompleted: (() -> Void)? = nil,
onDisposed: (() -> Void)? = nil
) -> Disposable
|
onDisposed는 Disposable에 대해 설명하고 하려고 생략했었는데,
이제 disposed에 배운 이상 위 코드는 말 안해도 알 것이라 생각함
해당 Observable이 구독 해제될 때,
즉, disposed() 메서드가 불릴 때 실행 될 클로저를 넘겨주는 것임!!
(onError나 onComplete으로 인해 dipsosed 될 때도 당연히 실행 됨)
따라서 다음과 같이
해당 Observable이 구독 해제 될 때,
등록해놓은 클로저가 실행된답니다 :)
(코드를 보면 onDisposed 클로저를 보실 수 있습니다 ㅋ)
4. ControlEvent의 경우, 해당 이벤트를 갖고 있는 인스턴스가 deinit 될 때 자동으로 dispose 됩니다
내가 이것 때문에 살짝 헷갈렸었는데 음 ..
다음과 같이 내가 ControlEvent를 통해 Observable을 구독할 때
import UIKit
import RxSwift
import RxCocoa
class SodeulViewController: UIViewController {
@IBOutlet weak var sodeulButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
sodeulButton.rx.tap
.subscribe(onNext: {
print("소들 버튼 눌림")
}, onDisposed: {
print("소들 버튼 disposed")
})
}
}
|
위처럼 Disposable에 대한 처리를 따로 해주지 않았음!!!!!!!!!1
(물론 Subscribe 메서드의 Result를 사용하지 않는다는 노란 에러는 뜸)
Dispoable을 받아서 deinit 될 떄 처리해주지도 않았고!!!
DisposeBag에 넣어두지도 않았음!!!!
근데!!!!!!!!!
엥..??? 제가 따로 dispose를 해주지 않았는데..
자동으로 disposed() 메서드가 불리며, onDisposed: 에 등록해놓은 클로저가 불림...
세상에 이럴 수가
이게 가능한 이유는 말이죠
코드를 까보면서 예상한 건데
ControlEvent를 만들 create 메서드를 통해 생성된 Observable에
take(until:)이라는 operator를 실행시키는데, 이때 파라미터로 deallocated를 넘겨줌!!
(deallocated를 넘겨주는 코드는 넘 ㅜ어려워서 생ㄹㄹ략 흐규흐규ㅠ)
이 코드를 본 내 생각은,
ControlEvent를 통해 Observable을 구독하는 경우,
해당 ControlEvent를 갖고 있는 객체(sodeulButton)가 deallocate 되기 전까지만 구독을 하기 ㅣ때문에
뷰가 사라져서 sodeulButton도 같이 deallocate 되었다면,
구독도 자동으로 중지되는 것이 아닌가 싶음!!
UIControlEvent를 가진 객체가 deinit되면,
당연히 해당 객체를 통해 생성한 Observable 또한 같이 쓸모가 없어지니...
자동으로 이때 해당 구독을 취소하는 작업도 진행하게끔 해놓은 게 아닐까
하는 생각이 듦...!!
(코드를 보고 해석한 건데, 만약 틀렸다면 피드백 주세요!!)
근데 ControlEvent의 경우 자동으로 dispose 된다고 해도,
명시적으로 disposeBag에 넣는 게 좋아 보입니다!! (노란 에러도 뜨기 때문)
음.. RxSwift의 포스팅의 텀이 길어지는 이유는 ㅠ
이해하기 쉽게 설명하려다 보니 생각보다 내용이 깊어지고, 자세해져서
이에대해 다시 공부하고 이해하고 쓰고 하느라 길어지네요 ㅠㅠ
Rx만 쓰다간 블로그 글 1달에 1개쓸 수 있으니
중간에 다른 공부도 포스팅 하도록 하겠읍니당ㅎㅎㅎㅎ
오늘도 읽어주셔서 감사하고,
오타 및 피드백은 언제나 환영입니다 :)!!!