signal과 driver 그리고 error handling에 대하여(RxSwift)
이 글을 읽기 전 eungding님의 블로그와 Rhyno님의 블로그글을 참고해보자.
Signal과 Driver?
Signal과 Driver는 메인쓰레드에서의 동작을 보장하고 error를 반환하지 않는다. 그런데 나는 ‘error를 반환하지 않는다는 것’이 에러가 난 것에 대해 스트림 종료 방지를 해준다는 의미로 처음에 받아들였다. 그래서 특정 Observable 뒤에 asSignal이나 asDriver를 써주면 해당 Observable에서 error가 난다고 하더라도 전체적인 스트림은 종료가 되지 않을 것이라고 생각했다.
그래서 몇가지 테스트를 진행해보았다.
@IBOutlet weak var button: UIButton!
let publishSubject: PublishSubject<Int> = PublishSubject()
_ = button.rx.tap
.subscribe(onNext: {
publishSubject.onError(TestError.fakeError)
})
_ = publishSubject.debug("PublishSubject")
.asSignal(onErrorJustReturn: 0).debug("Signal")
.emit()
//과연 스트림은 끊기지 않고 계속 유지될까?
enum TestError: Error {
case fakeError
}
버튼을 탭하는 경우 publishSubject로 error가 넘어가도록 했고 이를 미리 subscribe(emit)해준 스트림에서 받도록 세팅해두었다. 결과는 아래와 같았다.
Signal -> subscribed
PublishSubject -> subscribed
//버튼을 탭했고 publishSubject로 error가 넘어감
PublishSubject -> Event error(fakeError)
Signal -> Event next(0)
Signal -> Event completed
Signal -> isDisposed
PublishSubject -> isDisposed
시퀀스가 모두 disposed됐고 스트림이 종료되었다. Signal이든 Driver이든 Observable에 error가 있는 경우 시퀀스의 종료를 막아주는 것은 아니었다. 다만 에러가 난 경우 이를 이후에 어떻게 처리할 것인지를 설정할 수 있을 뿐.
flatMap에 적용해보기
@IBOutlet weak var secondButton: UIButton!
_ = secondButton.rx.tap.debug("TAP")
.flatMap {
return Observable<Int>.error(TestError.fakeError).debug("내부 Observable")
}.debug("flatMap 이후")
.asSignal(onErrorJustReturn: 0).debug("Signal 이후")
.emit()
버튼을 탭하는 경우 flatMap에서 error가 들어가있는 Observable이 반환되도록 설정하였다.
결과는 아래와 같다.
Signal 이후 -> subscribed
flatMap 이후 -> subscribed
TAP -> subscribed
//버튼 탭
TAP -> Event next(())
내부 Observable -> subscribed
내부 Observable -> Event error(fakeError)
flatMap 이후 -> Event error(fakeError)
Signal 이후 -> Event next(0)
Signal 이후 -> Event completed
Signal 이후 -> isDisposed
flatMap 이후 -> isDisposed
TAP -> isDisposed
내부 Observable -> isDisposed
역시 마찬가지로 전체 스트림은 모두 종료되었으며 Signal은 에러에 대한 후속처리만 담당해 준 것을 볼 수 있었다.
추가 테스트 1
asSignal을 위로 올리면 어떻게 될까?
@IBOutlet weak var thirdButton: UIButton!
_ = thirdButton.rx.tap.debug("TAP")
.asSignal(onErrorJustReturn: ()).debug("Signal")
.asObservable().debug("flatMap 이전")
.flatMap {
return Observable<Int>.error(TestError.fakeError).debug("내부 Observable")
}.debug("flatMap 이후")
.subscribe()
결과는 아래와 같다.
flatMap 이후 -> subscribed
flatMap 이전 -> subscribed
Signal -> subscribed
TAP -> subscribed
//버튼 탭
TAP -> Event next(())
Signal -> Event next(())
flatMap 이전 -> Event next(())
내부 Observable -> subscribed
내부 Observable -> Event error(fakeError)
flatMap 이후 -> Event error(fakeError)
Unhandled error happened: fakeError
flatMap 이후 -> isDisposed
flatMap 이전 -> isDisposed
Signal -> isDisposed
TAP -> isDisposed
내부 Observable -> isDisposed
모든 시퀀스는 disposed되었고 전체 스트림이 종료되었다.
추가 테스트 2
이번에는 throw를 써보았다.
@IBOutlet weak var fourthButton: UIButton!
_ = fourthButton.rx.tap.debug("TAP")
.map {
throw TestError.fakeError
}.debug("map 이후")
.asSignal(onErrorJustReturn: 0).debug("Signal 이후")
.emit()
map operator 내 throw TestError.fakeError
라고 쓴 부분을 return TestError.fakeError
라고 쓰는 경우 밑의 .asSignal(onErrorJustReturn: 0)
부분에서 오류가 발생한다.
오류 메시지: Cannot convert value of type 'Int' to expected argument type 'ViewController.TestError'
map을 통해 반환되는 데이터 타입인 TestError와 asSignal에서 오류에 대해 리턴해주는 0이라는 값 데이터 타입이 서로 맞지 않다는 것.
이 경우 return TestError.fakeError
에 맞춰 asSignal부분을 .asSignal(onErrorJustReturn: TestError.fakeError)`라고 써주면 에러는 사라진다. 하지만 오류에 대해서 또 오류를 내보낸다면 asSignal을 쓰는 이유가 없어지니..
내 추측이긴 하지만 map에서 return으로 오류를 반환 시 오류로 인식하지 못하고 하나의 데이터로 인식하는 것으로 보인다.
결과는 아래와 같다.
Signal 이후 -> subscribed
map 이후 -> subscribed
TAP -> subscribed
//버튼 탭
TAP -> Event next(())
map 이후 -> Event error(fakeError)
Signal 이후 -> Event next(0)
Signal 이후 -> Event completed
Signal 이후 -> isDisposed
map 이후 -> isDisposed
TAP -> isDisposed
모든 시퀀스가 disposed되었고 전체 스트림이 종료되었다.
비슷한 코드를 RxSwift Github - ‘CLLocationManager+Rx’ Example에서 볼 수 있다.
delegate.methodInvoked의 반환되는 Observable에서 map을 통해 특정 parameter만을 선택하고자 할 때 throwable한 function을 호출하여 형변환을 한다.
형변환이 잘 수행되면 원하는 파라미터 값이 시퀀스에 잘 실리지만 형변환에 실패하면 RxCocoaError.castingError가 throw된다.
만약 에러가 throw되는 경우 형태는 위와 비슷할 것이다.
다만 궁금한 것은 ‘에러가 throw되는 경우에 이 것이 Observable의 onError가 호출된 것과 같은 것일까?’ 이다. 시퀀스에 오류가 실린게 맞는걸까? flatMap에서 Observable<>.error가 반환되는 것과 map에서 error가 throw되는 것, 그리고 map에서의 throwable function 호출에 큰 차이가 없는 걸까?
추가 테스트 3
@IBOutlet weak var fifthButton: UIButton!
let observable: () throws -> Observable<Void> = {
throw TestError.fakeError
}
_ = fifthButton.rx.tap.debug("TAP")
.flatMap {
return try observable().debug("내부 Observable")
}.debug("flatMap 이후")
.asSignal(onErrorJustReturn: ()).debug("Signal 이후")
.emit()
실행 결과는 아래와 같다.
Signal 이후 -> subscribed
flatMap 이후 -> subscribed
TAP -> subscribed
//버튼 탭
TAP -> Event next(())
flatMap 이후 -> Event error(fakeError)
Signal 이후 -> Event next(())
Signal 이후 -> Event completed
Signal 이후 -> isDisposed
flatMap 이후 -> isDisposed
TAP -> isDisposed
“내부 Observable”이라고 표기한 debug메시지가 뜨지 않았다는 점이 특이하다. 아마도 throw로 에러가 나와서 그런 것일 듯. (정상적으로 Observable을 받았다면 메시지가 띄워졌을 것이다.)
정상적으로 Observable을 받은 경우에는 아래와 같이 메시지가 뜬다.
//observable 클로에서 Observable.just(())를 return 하는 경우
TAP -> Event next(())
내부 Observable -> subscribed
내부 Observable -> Event next(())
flatMap 이후 -> Event next(())
Signal 이후 -> Event next(())
내부 Observable -> Event completed
내부 Observable -> isDisposed
여기서 또 궁금한 것은, flatMap은 일반적으로 중첩된 Observable을 하나로 풀어주기 위해 사용하는데 위의 경우 flatMap은 어떻게 작동하냐는 것이다. 위에서 flatMap 내부의 ‘return try observable().debug(“내부 Observable”)’ 는 Observable을 반환하는 것이 아니라 단순히 Error를 throw하기 때문..
정리
signal과 driver가 error를 return하지 않는다고 해서 스트림이 종료되는 것을 막아주는 것은 아니다. error를 handling해서 스트림이 종료되지 않게 하는 것은 별개의 문제. 보통 catch나 catchAndReturn등 을 이용한다.
error는 Observable 안에서 onError()가 호출됨에 따라 넘어올 수도 있지만 throw를 통해 넘어올 수도 있다.
(throw의 경우 어떻게 작성해서 써야할까? Observable.create 내에서는 직접적으로 throw가 불가능한데..)
댓글남기기