SPM과 RxSwift 그리고 TestUnitBundle을 사용하면서 생기는 에러 사항들 모음집입니다.
저번글에서 MainApp에서의 에러 사항을 해결했습니다.
따라서 우리는 TDD를 적용하기 위해서 TestCase를 돌리려고 합니다.
cmd + u를 눌러 TestCase를 돌리게 되면 다음과 같은 에러가 나오게 됩니다.
1. Missing Required module "RxCocoaRunTime"
이것에 대한 해결방법은 아주 쉽습니다.
TestTarget에 RxTest,RxBlocking,RxCocoa를 추가해주시면 사라지게 됩니다.
요렇게 해만 해주면 에러가 사라지게 됩니다.
하지만 뭔가 꺼림칙한 메세지가 잔뜩 나옵니다.
요런식으로 코드가 중복됬다는 이야기가 나옵니다.
보니까 MainApp과 TestTarget에 있는 RxSwift를 중복으로 찾았고 둘중에 하나를 참조하겠다는 로그입니다.
이 문제는 찾아보니까 CocoaPod에서도 Carthage에서도 같은 상황이 발생할 수도 있다고 하는군요.
근데 문제가 오래되었는데도 불구하고 아직까지 정확히 왜? 이런 현상이 일어나고 있는지 그리고 뚜렷한 해결책이
나오지 않은 상황인 것 같습니다.
하지만 일단 빌드가 되는 것을 확인했으니 넘어가겠습니다.
2. failed to demangle superclass of ~~~
이제 빌드가 되는 것을 확인하고 즐겁게 테스트 코드를 통해 ViewModel을 테스트하다보면 다음과 같은 에러가 갑자기 생깁니다.
빌드가 되는 것 까지 확인하고 테스트를 돌렸는데 demangle어쩌고 저쩌고 이야기가 나오면서 crash가 일어납니다.
여기서 mangle, demangle에 대하여 살짝 이야기 해보자면
mangle이란 class 혹은 함수 같은 이름을 위에 보시는 바와 같이 \^A\M-I\M-y\M-~\M^?yxq_q0_G 혹은
_TtC7RxCocoa13ControlTarget처럼 조각내는 것을 말합니다.
왜 조각내느냐? 이것은 해당 이름이 고유한 것인지 컴파일러와 링커가 알아내기 위해서 입니다.
컴파일에서 부터 만들어진 코드는 런타임 혹은 빌드타임에서 링커에 의해 다른 프레임워크와 연결됩니다.
여기서 링커가 다른 위부 프레임워크와 링킹을 할때 보다 정확하게 찾아서 함수를 연결하기 위함이라고 합니다.
그리고 Demangle은 mangle된 이름을 다시 복호화하는 과정입니다.
이제 다시 에러 원문을 봐봅시다.
failed to demangle superclass of FlatMapSink from mangled name '\^A\M-I\M-y\M-~\M^?yxq_q0_G': subject type q0_ does not conform to protocol ObserverType
해석해보면 "\^A\M-I\M-y\M-~\M^?yxq_q0_G"이라는 mangled 이름이 있는데 이게 아마 FlatMap함수인가 봅니다.
실제로 제 코드에서는 flatMap이 맞습니다. 그리고 이걸 복호화 했더니 FlatMap의 superclass를 찾지 못하겠다는 이유입니다.
그래서 이걸 보고 제 머리속에는 2가지 정도 생각이 들었습니다.
1. 프레임워크가 중복되어 demangle 했을때 RxSwift를 제대로 찾을 수 없었다.
2. 혹시나 ViewModel이 그냥 중간에 deallocated 되었나.
그리고 실험을 해보니 역시나 2번의 경우는 아니였습니다. 그러면 이제 1번의 경우만 남았다는 것인데 좀 더 파보기 전에 실행이 되는 코드를 만들어 보겠습니다.
일단 제 코드는 다음과 같습니다.
class ViewModel {
struct Input {
let strInput : Observable<String>
}
struct Output {
let resultOutput : Observable<SomeThingForFlatMapResult>
}
.
.
.
func transform(input : Input) -> Output {
let resultoutput = input.strInput
.flatMap{ [unowned self] str -> Observable<SomeThingForFlatMapResult> in
return self.networkService.fetch().map { result -> SomeThingForFlatMapResult in
return SomeThingForFlatMapResult(someString: "", someInt: 1, someDouble: 0.0)
}
}
return Output(resultOutput: resultoutput)
}
}
//TestCode
//viewmodel 주입용
final class NetWorkMock : NetWorkDelegate {
func fetch() -> Observable<NetWorkResult<SomeThingForFlatMapResult>> {
return Observable<NetWorkResult<SomeThingForFlatMapResult>>.create { seal in
seal.onNext(.success(SomeThingForFlatMapResult(someString: "", someInt: 1, someDouble: 0.0)))
return Disposables.create {
}
}
}
}
func testSomething(){
let scheduler = TestScheduler(initialClock: 0)
let result = scheduler.createObserver(SomeThingForFlatMapResult.self)
let output = viewModel.transform(input: input)
output.resultOutput
.subscribe(onNext : { [unowned self] result in
})
.disposed(by: disposeBag)
scheduler.createColdObservable(
[.next(1, "abc"),
.next(2, ""),
.next(3, ""),
.next(4, "add")])
.bind(to: strINput)
.disposed(by: disposeBag)
scheduler.start()
}
그리고 이걸 다음과 같이 바꿉니다.
class ViewModel {
struct Input {
let strInput : Observable<String>
}
struct Output {
let resultOutput : Observable<SomeThingForFlatMapResult>
}
.
.
.
func transform(input : Input) -> Output {
//바뀐 부분
let resultOutput = PublishSubject<SomeThingForFlatMapResult>()
input.strInput
.flatMap{ [unowned self] str -> Observable<SomeThingForFlatMapResult> in
return self.networkService.fetch().map { result -> SomeThingForFlatMapResult in
return SomeThingForFlatMapResult(someString: "", someInt: 1, someDouble: 0.0)
}
}
.bind(to: resultOutput) //바뀐 부분
.disposed(by: disposeBag)
return Output(resultOutput: resultOutput)
}
}
//TestCode
final class NetworkMock : NetWorkDelegate {
func fetch() -> Observable<NetWorkResult<SomeThingForFlatMapResult>> {
return .just(.success(SomeThingForFlatMapResult(someString: "", someInt: 1, someDouble: 1.1)))
}
}
func testSomething(){
let scheduler = TestScheduler(initialClock: 0)
let result = scheduler.createObserver(SomeThingForFlatMapResult.self)
let output = viewModel.transform(input: input)
output.resultOutput
.subscribe(onNext : { [unowned self] result in
})
.disposed(by: disposeBag)
scheduler.createColdObservable(
[.next(1, "abc"),
.next(2, ""),
.next(3, ""),
.next(4, "add")])
.bind(to: strINput)
.disposed(by: disposeBag)
scheduler.start()
}
TestTarget의 NetWorkMock에서 Observable.create 대신 .just로 그냥 event를 방출해주고
ViewModel의 transform에서 변수를 하나 추가해서 바인딩을 하면 놀랍게도 테스트가 성공적으로 완료됩니다.
이게 왜 되는지.....
아무튼 코드를 바꾸면서 얻어낸 2가지는 TestTarget에서 Observable.create할 경우
AnonymousObservableSink 쪽에서 demangle 에러 발생 한다는 것
그리고 ViewModel쪽에서 map이든 flatMap이든 중간에 변수를 하나생성 해서 바인딩하지 않고 바로
Output으로 변수를 할당해버릴 경우 에러가 생긴다는 것입니다.
이 경험들을 바탕으로 왜 1번 즉 코드의 중복이 문제가 될 수 있는가에 대해서 조악하게나마 의견을 내보고자 합니다.
우선 RxSwift에서 flatMap,map,just등이 어떻게 동작하는지를 조금이라도 알아야 합니다.
다음 그림을 한번 보겠습니다.
위의 그림에서 보면 Map,FlatMap,just등등 들이 Producer를 subclassing하고 있으시다는 걸 알 수 있습니다.
그럼 여기서 Producer가 뭘까요?
Producer를 보시면 run과 subscribe가 있는 것을 알 수 있습니다.
subscribe함수를 보면 let disposer = SinkDisposer()가 있으시다는 것을 알 수 있을 겁니다.
이 클라스가 중요한 이유가 바로 strong retain cylce을 발생시키면서 메모리를 계속 잡고 있는다는 것입니다.
let disposer = SinkDisposer()
let sinkAndSubscription = self.run(observer, cancel: disposer)
disposer.setSinkAndSubscription(sink: sinkAndSubscription.sink, subscription: sinkAndSubscription.subscription)
이 세줄 을 보시면 SinkDisposer를 생성하고 self.run을 하여 sinkAndSubscription을 만듭니다.
그리고 이걸 다시 disposer에 입력합니다.
이때 sink: sinkAndSubscription.sink이 변수가 해당 Operator가 이벤트 방출을 위한 모든 정보를 들고 있게 됩니다.
이러한 이유로 인하여 sink와 sink를 들고 있는 SinkDisposer를 strong retain cycle로 메모리를 붙잡고 있는 것입니다.
그리고 run 함수는 rxAbstractmethod()즉 추상화 함수 밖에 없다는 것을 알 수 있습니다.
이유는 Producer를 subclassing하는 모든 클라스에서 그에 맞는 로직으로 run을 작성해야 한다는 것입니다.
예를 들어 FlatMap의 경우 다음과 같이 run함수가 정의 되어 있습니다. 그리고 보시는 바와 같이 전용 Sink클라스도 정의되어 있습니다.
flatMap이 어떻게 작동하는지에 대한 설명글이 아니라 왜 코드의 중복이 문제를 일으킬 수 있냐에 대한 글이므로 설명은 여기까지 하고
넘어가겠습니다. 자세한 동작 방식을 원하시는 분은 다음 링크를 읽어주시면 될것 같습니다.
(https://medium.com/@polidea/8-mistakes-to-avoid-while-using-rxswift-part-1-e45b21b47649)
이제 대충 눈치채실 분들이 있을 겁니다.
바로 FlatMap의 대한 정보가 어디 shared storage에 저장이 되는 것이 아니고 RxSwift 프레임워크 안에 저장이 되어 있다는 게 문제를 일으키는 것입니다.
즉 코드의 중복으로 인해서 testCode를 돌릴때 ViewModel에서 실행되는 코드들은 MainBundle안에 있는 RxSwift를 참조하고
TestBundle안에 있는 함수는 TestBundle안에 있는 RxSwift를 참조한다는 것입니다.
근데 FlatMap에 대한 모든 정보는 MainBundle쪽 프레임워크안에 있으니 TestBundle에서 이것을 받아 subscription을 실행할때
sink에 대한 정보가 하나도 없으니 에러가 나는 것입니다.
이러한 이유로 sink의 과정이 없는 PublishSubject를 통해서 할 경우는 되고 flatMap을 바로 돌려주는건 안되는 것입니다.
앞서 말한 Observable.create경우에도 return Disposables.create 경우에 AnonymousObservableSink라는 객체가 있어서 이번에는 반대로 ViewModel쪽에서 TestBundle에서 넘겨준 스트림을 받지 못하는 것입니다.
정말 두서없이 글을 쓰게 되었습니다만, 문제의 원인은 매우 정확해 졌고 이로 인해 해결 방법또한 길이 정해졌습니다.
같은 프레임워크를 참조하게 만들면 만사 ok이다 라는 것 입니다.
그러면 어떻게 중복 참조하지 않게 하나를 하면 일단 3가지 방법이 떠오릅니다.
1. RxCocoa 자체를 TestBundle에 추가하지 않는다.
- RxCocoa자체가 RxSwift를 참조하게 되어 있습니다. 따라서 TestBundle에 RxCocoa만 추가했는데도 불구하고 RxSwift와 RxRelay의 복사본이 생긴 것입니다.
근데 우리가 RxCocoa를 추가한 이유는 RxCocoaRuntime이라는 것을 가지고 오기 위해서 였습니다.
이러기에 RxCocoaRuntime만 가져와서 코드를 복사하든 따로 프레임워크로 만들어서 추가하든 하는 방법이 있을 수도 있습니다.
2. 제 3의 외부 의존성 패키지 프레임 워크를 만들어서 이것을 MainBundle과 TestBundle에서 공통적으로 사용한다.
3. TestBundle의 FrameWork Search Path혹은 다른 Path를 강제로 Main쪽으로 연결한다.
해결 방법에 대한 과정들은 다음글에서 이어나가겠습니다.
'iOS' 카테고리의 다른 글
CFSocket을 통한 로컬 서버 만들기 1 (0) | 2023.05.29 |
---|---|
Rx와 XCTest를 이용할시 생기는 에러사항들 3 (0) | 2023.03.27 |
Rx와 XCTest를 이용할시 생기는 에러사항들 1 [library not loaded XCTest] (0) | 2023.03.26 |
JENKINS(4) github webhook (2) (0) | 2023.03.11 |
JENKINS (3) - github Webhook[ngrok] (1) (1) | 2023.03.11 |