cchanmi
[RxSwift] RxSwift의 메모리 누수에 대해서 본문
RxSwift는 비동기 작업 코드를 쉽게 관리할 수 있게 도와주는 라이브러리입니다. 그로인해 코드의 가독성이 높아지고, 다양하고 편리한 Operator를 활용하여 효율적인 비동기 처리가 가능해집니다.
그런데 이러한 RxSwift에는 큰 단점이 있는데, 잘못된 사용으로인한 메모리 누수가 발생할 수 있다는 것입니다.
오늘은 RxSwift에서 어떤 경우에 메모리 누수가 발생하는지, 해당 문제를 해결하기 위해서는 어떻게 해야 하는지에 대해 정리해 보겠습니다.
weak self
가장 기본적인 방법입니다.
button.rx.tap
.subscribe(onNext: { [weak self] text in
guard let self = self else { return }
self?.mapCountLabel.text = text
})
.disposed(by: disposeBag)
weak self는 중첩된 클로저가 많아지면 코드가 복잡해지고, 가독성이 떨어질 수 있습니다.
또한, 매번 옵셔널 바인딩으로 self의 존재를 확인해 주는 코드도 작성해 주어야 하는 번거로움이 있습니다.
weak self를 대체하기 위해 RxSwift6에서는 withUnretained가 추가되었습니다.
withUnretained
button.rx.tap
.withUnretained(self)
.subscribe(onNext: { owner, text in
owner.mapCountLabel.text = text
})
.disposed(by: disposeBag)
withUnretained를 사용하면 좀 더 간결하고 명확하게 코드를 작성할 수 있습니다.
처음에 withUnretained를 사용할 때 옵셔널 바인딩을 해 주지 않아도 되는 부분 때문에 약한 참조로 전달되고 있는 것이 맞나?라는 생각이 들어서 내부를 살펴보았는데
내부에서 guard let을 통해 옵셔널 바인딩을 해 주고 있는 것을 볼 수 있습니다.
Observable.of("테스트")
.withUnretained(self)
.subscribe(onNext: { owner, text in
print(text)
}, onError: { error in
print(error)
}, onCompleted: {
print("onCompleed")
}, onDisposed: {
print("onDisposed")
})
.disposed(by: disposeBag)
withUnretained는 onNext를 제외한 onError, onCompleted, onDisposed에서는 인스턴스를 전달해 주지 않는 것을 볼 수 있습니다.
만약 위 3가지 상황에 인스턴스를 전달받아 사용하고 싶은 경우에는 withUnretained를 사용하기 어려울 것 같다는 생각이 드네요.
하지만 withUnretained에도 큰 단점이 있었습니다.
RxSwift PR에 올라온 내용입니다.
https://github.com/ReactiveX/RxSwift/pull/2290
Unfortunately there is a small side-effect we didn't consider. If the stream you're working with has share(replay: 1), the replay buffer would buffer self along with the value, which might create a retain cycle in some scenarios.
만약 withUnretained를 사용하는 스트림이 share(replay:1)을 사용 중이라면 replay buffer가 self와 함께 값을 버퍼링하여 특정 상황에서 순환 참조를 발생시킬 수 있습니다.
This in itself isn't an issue (just a documentation change), but for Driver specifically, the default is to replay the latest value, so we are forced to immediately deprecated withUnretained on Driver, specifically. It is still available on all other traits, Observable and Signal.
특히, Driver의 경우 기본적으로 최신 값을 replay 하기 때문에 Driver를 사용할 때에는 withUnretained를 사용해서는 안됩니다. Observable, Signal 등에서는 사용할 수 있습니다.
일단 replay buffer가 self와 함께 값을 버퍼링한다는 게 무슨 뜻인지 잘 모르겠네요... 이부분에 대해서는 공부가 필요해 보입니다.
아무튼 순환 참조를 방지하기 위해 사용하는 operator인데 특정 상황에서 순환 참조가 발생하면 안되겠죠?
그래서 RxSwift 6.1부터 withUnretained 대신 subscribe(with:onNext:onError:onCompleted:onDisposed:)를 추가했습니다.
subscribe(with:onNext:onError:onCompleted:onDisposed:)
Observable.of("테스트")
.subscribe(with: self, onNext: { owner, text in
print(text)
}, onError: { owner, error in
print(error)
}, onCompleted: { owenr in
print("onCompleed")
}, onDisposed: { owner in
print("onDisposed")
})
.disposed(by: disposeBag)
해당 메서드는 withUnretained와 다르게 모든 상황에서도 인스턴스를 전달해 주는 것을 볼 수 있습니다.
역시나 내부에서 guard let을 통해 옵셔널 바인딩을 해 주는 것을 볼 수 있습니다.
'iOS' 카테고리의 다른 글
[iOS] Github Action 도입기 - build & test 과정에서 겪은 error 모음 (1) | 2024.12.01 |
---|---|
[iOS] Github Action 도입기 - build & test (2) | 2024.11.30 |
[iOS] UIKit+Combine 환경에서 CollectionView Cell 바인딩 문제 해결 (1) | 2024.09.07 |
[iOS] RxSwift 정리 (1) | 2024.09.04 |
[iOS] CollectionView Cell 재사용에 따른 중복 binding 이슈 해결 (0) | 2024.08.31 |