어쩌면 저희가 프로젝트 내내 고민했던 부분은 이 고민으로 통한다고도 할 수 있겠네요.. Delegate 함수를 통해 받아볼 수 있는 결과값을 어떻게 Rx의 데이터 스트림에 태울 수 있는가. 애플 로그인을 할 때 고민했고, 오류 처리를 어떻게 해야 할 지 고민했으며, 샤잠킷의 결과값을 오류와 검색 결과 모두 받아볼 수 있게 하려면 어떻게 해야 할 지 고민하기도 했죠. 사실 주간 달 세뇨는 이걸 해결하기 위해 써 온 것이 아닐까? 하는 생각도 합니다. 또한 반응형 프레임워크를 쓴다면 앞으로도 가장 포괄적으로 써먹을 수 있는 주제가 아닐까? 싶기도 하구요.
서론이 길었네요? 바로 넘어가 보겠습니다.
주간 달 세뇨 1편에서 처음 말씀드렸듯, 저희는 이번 프로젝트를 통해 클린 아키텍처에 대해 명확히 알고 가고자 했습니다. “역할에 따른 코드의 분리”는 이제 저희의 작업에 있어 일종의 명제처럼 기능하게 되었죠. 볼 수 있는 곳에 코드를 욱여넣는다면 어떻게든 작동하는 앱은 만들 수 있었을 겁니다. 하지만 저희는 구조를 최대한 엄격하게 지키고 분리하려고 노력했습니다. 각자 관련된 경험이 있기 때문에 더 그랬던 건지도 모릅니다. 저(낭만)의 경우는 부캠 학습 스프린트를 수행하던 도중, 뷰 컨트롤러에서 이런 걸 들고 있어야 하나? 라는 위화감을 해소하기 위해 코드를 분리해 봤고, 당시에는 그것을 Session이라고 명명했던 것으로 기억합니다. 정말 자연스럽게 클린 아키텍처의 형성 과정을 경험한 것이랄까요.. 모두가 비슷한 경험을 했고, 그래서 합의가 되었죠.
그래서, delegate을 통해 값을 반환하는 퍼스트 파티 SDK 관련 코드들을 전부 Session이라는 이름 하에 분리하고, 여기서 얻은 결과값을 Repository - UseCase - ViewModel을 거쳐 View에 반영되게끔 구현했습니다. 첫주차부터 이걸 고민해서 그런지, 지금에 와서는 그래도 어찌어찌 요령껏 결과를 이어 줄 수 있었습니다. 이 과정에서 저희가 집중한 부분은, import를 최소화하는 겁니다. 예를 들면 ViewModel에서 UIKit
요소를 가지고 있는 AuthenticationServices
가 import되면 안 되겠죠. 뷰 관련된 요소는 ViewController나 Coordinator에서만 알아야 하니까 말입니다. 이걸 지키면, 역할이 비슷한 코드들을 Session에 모아 주는 작업의 80% 정도는 저절로 수행이 되었습니다.
예. 20%가 남지요? 그것은 바로 이 delegate 메서드 반환값들을 어떻게 Rx의 데이터 스트림에 태워서 뷰에 반영하느냐 하는 거였습니다. 결과가 올바르게 나왔다면 뷰에 결과를 띄워 주고, 에러가 떴다면 팝업을 통해 알려 줘야겠죠. 성공과 실패? 오! Result Type! 라는 생각으로, 저희는 Rx의 PublishSubject 안에 typealias ShazamResult = Result<ShazamSong, ShazamError>
하는 식으로 타입을 만들어서 넣어 줬습니다. onError를 사용하는 것에 대한 고민을 2편에서 했듯 잘 안 됐고, 그냥 에러가 났어! 결과가 왔어! 하는 값을 넘기기만 하면 될 것 같다고 생각을 했습니다. 조금 찝찝하긴 했지만요. 아니나 다를까, 왜 슬픈 예감은 틀리질 않는지.
정확히는, 음악 재생 기능을 만들 때 오류가 발생했습니다. 재생이 실패한 경우, 안 되는 이유를 에러로 만들어서 그동안은 print를 통해 콘솔에 찍어 주기만 했는데, 이제 그걸 팝업을 통해 유저에게 알려주려고 했을 때 문제가 된 겁니다. 이 때 발생한 오류가 RxSwift 오류 중 Reentrancy anomaly was detected 오류였습니다. 사실 지금도, 이 오류에 대해 그렇게 아주 잘 아는 것은 아닙니다. 일단 에러 텍스트를 볼 수 있겠습니다.
⚠️ Reentrancy anomaly was detected.
> Debugging: To debug this issue you can set a breakpoint in (RxSwift 경로) and observe the call stack.
> Problem: This behavior is breaking the observable sequence grammar. `next (error | completed)?`
This behavior breaks the grammar because there is overlapping between sequence events.
Observable sequence is trying to send an event before sending of previous event has finished.
> Interpretation: This could mean that there is some kind of unexpected cyclic dependency in your code,
or that the system is not behaving in the expected way.
> Remedy: If this is the expected behavior this message can be suppressed by adding `.observe(on:MainScheduler.asyncInstance)`
or by enqueuing sequence events in some other way.
구글링을 해 봐도 이 오류가 일어나는 상황은 굉장히 제각각이더라구요. 하지만 오류 메시지를 읽어 보면서 대략적인 오류의 얼개를 잡아볼 수 있었습니다.
.observe(on: MainScheduler.asyncInstance)
를 더해 보라는 데에서 유추뭐, 원래 그런 의도기는 합니다. 결과 타입에서 실패와 성공을 모두 컨트롤하도록 설계를 한 거니까요.