[iOS] combine의 catch는 어떻게 동작할까(1) 에 이은 두번째 포스팅입니다!
이어지는 내용이니 이번포스팅에서는 거두절미하고 바로 본 포스팅으로 가보겠습니다 ㅎㅎ
1편에서는 send이전에 실질적으로는 conduit을 만들어서 값을 보내는쪽과 최종적으로 받는쪽에 전달하는 과정을 따라가봤습니다, 이번 포스팅에서는 실제 값이 어떤 흐름을 거쳐서 최종적인 subscriber로 전달되는지에 대해 알아보겠습니다
subject
.tryMap { value in
if value % 2 == 0 { throw CustomError.even }
return value
}
.catch { _ in
Just(100)
}
.sink { value in
print(value)
}
.store(in: &cancelBag)
subject.send(2)
이번 포스팅에서도 앞선 포스팅과 같은 예시코드를 사용해보곘습니다
우선 .send(2)가 실행됬다고 해볼까요?
현재 passthroughSubject는 conduit을 하나만 가지고있기때문에 conduit의 offer메서드를 실행시키고
conduit의 offer를 실행시키면 conduit의 downstream의 recieve메서드를 호출시킵니다
현재 conduit의 downstrea은 TryMap.Inner이기때문에 TryMap.Inner의 receive메서드를 호출시킵니다
현재 TryMap.Inner의 Map은 tryMap을 실행할때의 저장된 클로저 이고
.tryMap { value in
if value % 2 == 0 {
throw CustomError.even
}
return value
}
위의 클로저입니다 input값인 2를 가지고 해당클로저를 먼저실행합니다, 이때 2라는 값이 error를 throw하기에 catch문이 실행되게 되고 downstream인 Catch.Inner.UncaughtS의 recieve(completion:_)을 실행시키게됩니다
그러면 Catch.Inner.UncaughtS의 inner의 receivePre(completion:_)을 실행하게되는데 여기서 inner는 Catch.Inner입니다, 이때 .failure(error)를 인자로 넣어줬으니
해당 메서드에서 failure case가 실행되게 됩니다
그리고 이때 status를 .pre에서 .pendingPost로 바꿔주게 됩니다
이렇게 하는 이유에 대해 추측을 해보면 error가 발생하지않을때는 값을 post했겠지만 error가 발생해서 아얘 다른 값을 보내야하기에 pending(보류)라는 단어를 써서
error가 아닐때 어떤값을 보내려고헀는데 error가 발생했으니 어떤값이 가진 않을거니까 어떤값이가는 상황은 우선 보류~
가 아닐까 합니다
그리고 나서 handler에 error를 input으로 넣어주고 클로저를 실행해주는데 그러면 결과가 새로운 publisher가 됩니다, Catch를 호출할때 publisher를 반환해주는 클로저를 넣어놨으니까요
handlr(error)의 결과는 Just(100)이므로
Just(100).subscribe(CaughtS(inner: self))와 동일한 코드가 됩니다
그러면 결국 Just(100)이라는 publisher에 subscribe()메서드를 호출해주고 CaughtS라는 객체를 넣어주는데 이 CaughtS의 inner는 self니까 Catch.Inner가 들어가게됩니다
이런 Catch.Inner.CaughtS라는 객체가 들어가게 되겠네요
그러면 늘 반복되는대로 Just라는 publisher에 subscribe()메서드가 호출되었으니 receive(subscriber:_)라는 메서드가 연쇄적으로 호출되게 되고
Catch.Inner.CaughtS의 recieve(subscription:_)이 호출되면서 Just.Inner를 넣어줍니다
Just.Inner는 value로 100이라는값이 들어가고 downstream으로 Catch.Inner.CaughtS를 가지고 있습니다
결국은 이런 Just.Inner의 형태가 되겠네요
Catch.Inner.CaughtS의 recieve(subscription:)이 호출되면 inner인 Catch.Inner의 receivePost(subscription:)이 호출되면서 그대로 Just.Inner를 넘겨줍니다
그러면 Catch의 state를 이제는 catch가 발생했고 catch로인한 값이 전달될것이기때문에 pre(추후에 간다)가 아닌 post로 바뀌게됩니다
그리고 subscription인 Just의 request메서드가 호출되면 Just.Inner의 downstream인 Catch.Inner.CaughtS의 receive(100)이 호출되게 됩니다
Catch.Inner.CaughtS는 inner인 Catch.Inner의 .receivePost(100)을 호출하게되고
Catch.Inner의 downstream인 sink에 값을 전달하게 됩니다
그러면 마지막으로 sink내부의 클로저에 value를 넣어서 클로저를 실행하게되고 sink의 클로저는 값을 받아서 print해라!
였으니까 100을 호출하고 끝나게됩니다
조금 헷갈릴수도있는부분이 많아서 너무 자세한 내용을빼고 핵심만 요약을 잠시 해보겠습니다
위에서 어떤 메서드가 불리고 어떤메서드가 불린다 보다는 값이 Catch.Inner.UncaughtS에서 Inner인 Catch.Inner로 전달되고 Catch.Inner의 downstream인 sink로 전달되는구나 정도의 큰그림만 알고계서도 충분히 좋을거같습니다!
catch는 기본적으로 error가 발생하지 않을 case를 우선적으로 고려해 sink를 연결합니다(send이전에요)
그렇기 때문에 기본적으로 TryMap와 error가 발생하지 않은상황이라는 네이밍의 Catch.Inner.UncaughtS와 stream을 연결합니다 하지만 에러가 발생한다면 애초에 error가 발생하지 않을때의 청사진이 담긴 객체가 아닌 error가 발생했을때의 청사진이 담긴 객체와 연결을 해야하기에 error가 발생하면 그때가서 Catch.Inner.CaughtS를 만들어서 Catch의 클로저를 실행시켜 나온 Publisher(예시에서는 Just)와 연결시키는 과정을 다시 거치게 됩니다
그렇게 해서 새로운 publisher에서 방출된 값이 CaughtS의 Inner인 Catch.Inner로 전달되고 그값이 Catch.Inner의 downstream인 sink로 전달되게 되는 로직인겁니다!
그렇다면 에러가 발생하지 않는 상황을 보기전에 예측을 해볼까요?
이미 send이전에 error가발생하지 않을때의 청사진이 담긴 Catch.Inner.UncaughtS를 만들어서 TryMap과 sink를 연결해놨기 때문에 tryMap에서 어떤 값이 방출되면(error가 없으면 mapping한 값이 방출되겠죠) 그대로 Catch.Inner.UncaughtS의 Inner인 Catch.Inner로 값이 전달되고 Catch.Inner의 downstream인 sink로 값이 전달되게 되지않을까 라는 예상을 해볼 수 있습니다
예상이 맞는지 한번 확인해보러 갑시다!
error를 던지지 않을때의 청사진을 conduit이 우선가지고 있는 상황이고 conduit은 위와 같은 구조를 가지고 있습니다
error를 발생시킬때나 아닐때나 conduit의 offer메서드를 실행시키면 conduit의 downstream의 receive()를 실행시키고 conduit의 downstream인 TryMap.Inner의 receive()가 호출됩니다
1이라는 값이 send되었을때를 가정해보면 1은 2로나누었을때 나머지가 0이 아니기때문에 error를 throw하지 않고 값이 그대로 return되기때문에 그대로 TryMap.Inner의 downstream에 값이 전달됩니다
그리고 다시 TryMap.Inner의 downstream인 Catch.Inner.UncaughtS에 값이 전달되고 Catch.Inner.UncaughtS의 Inner인 Catch.Inner가 1이라는 값을 받게되고 최종적으로 Catch.Inner의 downstream인 sink가 값을 받아 print하라는 클로저의 input으로 들어가고 실행시켜 1이라는 값이 print되게 됩니다
컴퓨터구조를 공부할때 이런 비슷한 느낌의 파트가있었던게 기억에 나네요 ㅎㅎ 어떤 상황이 발생할확률이 평균적으로 낮을때 우선 그 일이 발생하지 않는다고 가정해놓고 미리 청사진을 짜놓고 만약에 확률이 낮은 상황이 발생한다면 그때 그 상황에 맞는 청사진을 만들어서 처리를하는게 효율적인 방식이었다라는걸 공부했던 기억이납니다
(정확히 어떤 챕터였는지는… branch였던거같은데 생각나면 링크 올려놓겠습니다)
암튼 그런느낌으로 생각한다면 어떤 앱에서 error가 발생할확률보다는 당연히 error가 발생하지 않을 확률이 높기에 우선 send전에 데이터전달에 대한 청사진을 짤때(이게 이제 conduit을 만드는거겠죠)
error가 발생하지 않았을때의 청사진을 우선 만들어서 효율적으로 동작하게 하는 구조가 아닐까라는 생각이듭니다(Uncaught라는 네이밍에서 그런 느낌이 들더라고요)
결국 catch는 여러operator(catch를 포함해서)를 거쳐 subscriber에게 가는 데이터의 path를 error가 발생하지 않은 경우에 만들어줍니다, conduit형태로요
conduit을 보면 이렇게 해석할수있습니다
- UncaughtS가 downstream으로 있으니까 이건 중간에 error가 발생하지 않을때 PassthroughtSubject에 들어온값이 TryMapt.Inner로 들어간다
- 그 값이Catch.Inner.UncaughtS로 들어간다
- 다시 그 값이 Catch.Inner로 들어가서 Sink라는 subscriber로 전달된다
결국 Catch.Inner가 TryMap과 Sink를 그대로 연결해주게 된 상황이 됩니다
하지만 중간에 TryMap에서 error가 throw된다면 TryMap.Inner의 downstream을 Catch.Inner.Caught로 바꿔주고 Catch.Inner가 Just라는 recovery Publisher(예제에서는Just)
를 만들어서 이 Publisher와 Sink를 연결시켜주게됩니다
이 상황을 그림으로 표현한다면 이렇게 표현할 수 있겠네요
결국 catch는 catch를 호출한 upstream과 downstream을 연결시켜줄지 아니면 catch의 recovery Publisher와 downstream을 연결시켜줄지를 결정해주는 역할을 하는 operator다
라는 결론을 드디어 내릴수있게되었습니다 ㅎㅎ…(error가 발생하지 않아도 catch쪽으로 데이터가 들어가긴 하겠네요)
마지막으로 왜 굳이 catch는 error가 발생했을때 새로운 publisher를 return하는걸까에 대한 고민을 해봤었는데요
그 고민에 제 나름의 답을 내려서 제 생각을 적어볼까합니다
만약에 어떤 viewcontroller에 label에 text와 image를 data를 받아서 넣는다고 생각을 해볼까요? 아래와같은 UI가 있다고 해볼게요
UI를 짠다고 생각해보면 킴스캐슬이라는 string과 노란색 영역에 들어갈 이미지 url 그리고 날짜를 서버에서 받아올겁니다 아마 보라색 기본이미지의 layout은 날짜 label옆에 있게 기본적으로 잡혀있겠죠
근데 서버에서 에러가 발생해서 데이터를 안주고 이름,이미지,날짜에 넣어줄 데이터가 아무것도 안오면 UI가 어떻게 될까요?
아마 error처리를 잘해놨다면 바로 다시 서버에 통신을 요청하겠지만 유저는 잠깐동안 아래와같은 UI의 layout이 엉망이된 view를 보게될겁니다
만약에 서버통신을 재시도 해서 바로 올바른 값이 들어왔다면 다행이겠지만 그게아니라면 유저는 이상한 UI의 화면을 계속 봐야할겁니다
그런데 catch를 통해 임시 데이터를 publish해주면 어떨까요
예를들어서 "사용자"라는 이름과 기본이미지 url그리고 "00월00일"이라는 데이터를 보낸다면 오류가 발생하더라도 처리하는 동안
최소한 layout이 깨져 엉망인 UI를 보여주지는 않을수있을겁니다
제가 생각했을때 catch가 error를 받았을때 같은 output type의 publisher를 반환해주는 이유가 이런 이유이지 않을까라는 생각을 하게되었습니다
catch관련 pr에 코드리뷰를 남기다가 이런 생각을 하게 되었답니다 ㅎㅎ..
길고 길었던 catch에 대한 포스팅이 끝을 맺었네요 ㅎㅎ...
되게 쉬운 개념의 operator인줄알고 덤볐다가 생각보다 양이 방대하고 로직이 길고 복잡해서 정리하다보니 중첩타입이 너무 많아져서 저도 보면서 많이 헷갈리더라고요
그래서 최대한 그림을 많이 넣어서 이해하기 쉽고 흐름을 볼수있게 정리를 최대한 해봤습니다
정리하고보니 굳이 앞단의 복잡한 코드의 실행 순서는 몰라도 될것같다는 생각이드네요 그래도 catch라는 operator가 어떻게 error의 발생에 반응하는지에대한 그림이 그려질 정도로는 공부를 하게된것같습니다 ㅎㅎ
오늘 포스팅은 여기서 마무리해보도록 하겠습니다!
그럼 20000!