안녕하세요 킴스캐슬입니다:)
한 한달동안 combine관련주제들만 가지고 포스팅을 작성하는것같은데요...
combine만해도 아직 써야할게 너무 많이 남았네요...
지금진행하고있는 combine 스터디는 이제곧 끝이나지만 스터디를하면서 배우고 공부했던것들은 꼭 기록으로 남겨놔야할것같아서 이렇게 포스팅주제를 적어놨는데 이번 포스팅을 다써도 6개나 남아있군요...
오늘은 combine에서 error를 처리할 수 있는 대표적인 operator인 catch를 한번 분석해봤습니다
combine을 사용해서 프로젝트를 리팩터링할때 가장 까다로웠던 부분이 error를 처리하는 부분이었습니다 다른 operator에 비해서 레퍼런스도 찾기가 쉽지 않았고 catch자체의 매커니즘이 좀 와닿지 않은부분이있었거든요...
그래서 한번 열심히 공부를 해봤고 그 내용을 공유해보고자 합니다!
해당 주제의 포스딩은 두번으로 나눠서 업로드될 예정이며
(1편은 send이전의 catch, 2편은 send이후의 catch)
이번포스팅도 오픈소스라이브러인 opencombine을 활용해서 작성되었습니다
노력의 흔적들
이번 주제도 정말 빡세게 준비해봤습니다 이틀정도 코드분석하고 정리했던것같아요ㅎㅎ
catch가 어떻게 동작하는지를 알기전에 적어도 catch라는 녀석이 어떤 친구인지를 알아야겠죠?
공식문서를 보면 다른 publisher로 대체하므로써 업스트립 publisher의 error를 handle하는 친구라고 합니다
이 설명이 조금 어려울수도 있는데 업스트립에서 error가 발생하면 catch가 return해주는 publisher를 downstream과 연결시켜주는 친구라고 보시면 좋을거같습니다
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)
아주 간단한 예시를 들어보겠습니다
우선 sink라는 downstream이 연결이 되어있습니다, catch의 upstream은 trymap이고 downstream은 sink이라고 보면 되겠죠
근데 만약에 들어온 value가 2로 나누어떨어져서 .even이라는 error를 throw하면 catch입장에선 upstream인 trymap에서 error가 발생한 상황이겠죠. 그런경우에 catch의 recovery publisher인 Just(100)을 sink와 연결시켜줍니다
WWDC에서 catch가 반환하는 publisher를 recovery publisher라고 부릅니다!
즉 2를 send하게되면 sink가 Just(100)과 연결되어 100이라는 값을 받게됩니다
하지만 1을 send하게 되면 catch가 upstream인 trymap과 sink를 그대로 연결시켜주게 되어서 1이라느 값이 print되게 됩니다
이런식으로 catch를 통해서 subscribe되는 downstream은 error를 절대 받을수없게되겠죠, 어차피 상위에서 error가 발생하면 catch가 다른 stream으로 바꿔서 연결시켜주니까요
그리고 이런식으로 error가 절대발생하지 않는타입을 Never라고합니다, 이렇게 catch를 사용하면 error가 절대발생하지 않는 Never type이 output type으로 결정되게 됩니다
이제 catch가 어떤 친구인지 알았고 아주큰 메커니즘은 알았으니 그럼 도대체 어떻게 내부적으로 동작하는지를 알아보겠습니다
operator의 원리를 알고 보시면 좋을거라 생각해서 관련링크를 함께 남겨놓겠습니다
operator의 동작원리가 궁금하다면?
예시로 볼 코드는 위에서 봤던 코드와 동일합니다!
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)
우선 .tryMap에 의해서 TryMap이라는 구조체가 만들어질겁니다
upstream으로 tryMap이라는 메서드를 호출한 passthroughSubject가 들어갈거고 transform에는 tryMap을 호출할때 명시해준 클로저가 들어가겠죠
TryMap이라는 객체는 위의 파란색모양처럼 생겼을겁니다 그리고 TryMap이라는 객체가 catch라는 메서드를 실행하게됩니다, TryMap이라는 객체자체가 Publisher니까요, 그리고 비슷하게 catch라는 메서드를 실행하면 Catch라는 객체가 만들어집니다
operator메서드를 실행하면 operator이름을 가진 객체가 만들어진다라고 생각하시면 이해하시기 편하겠네요
Catch라는 객체에 upstream에는 self가 들어가는 self는 catch를 호출한 주체니까 여기서는 TryMap이라는 객체가 들어가게 되겠네요 그리고 handler에는 catch메서드를 호출할때 넣어준 클로저가 들어갈겁니다
Catch의 upstream에는 TryMap이들어가는데 TryMap이 위에서만든 파란색 모양의 객체여서 그대로 넣어줘봤습니다 Catch의 upstream은 TryMap이고 TryMap의 upstream은 PassthroughSubject인 그런 상태입니다
그러고나면 비로소 sink라는 메서드가 호출됩니다
sink라는 메서드를 통해서 Sink라는 객체를 만들고 여기서는 receiveValue라는 클로저만 저장을합니다
그리고 subscribe()라는 메서드를 실행시키는데 이 메서드를 실행시키는 주체인 self는 당연히 Catch라는 객체가 될겁니다
Catch를 포함해 Publisher의 subscribe()는 receive(subscriber:)라는 메서드를 연쇄적으로 호출합니다 Catch가 receive(subscriber:)라는 메서드를 실행시키고 인자로 sink객체를 전달해줍니다
Catch가 receive(subscriber:_)라는 메서드는 어떤 동작을 하는지 보겠습니다
Catch내부의Inner를 만드는데 중첩타입이기때문에 헷갈릴수가있어서 타입을 명시해보자면 Catch.Inner라는 객체를 생성해주게됩니다
Catch.Inner내부의 downStream으로는 sink라는객체가 들어가게되고 handler라는 클로저를 그대로 전달해줍니다
그런데 그렇게만든 Catch.Inner를 Catch.Inner.UncaughtS라는 객체를 만들때 initalize로 넣어줍니다
왼쪽에 있는 객체가 Catch가 만든 Catch.Inner이고 오른쪽 초록색 객체가 Catch.Inner을 inner로 가지고 있는 Catch.Inner.UncaughtS라는 객체입니다
이렇게 새로운 객체를 만들어준다음에 Catch의 upstream인 TryMap의 subscribe메서드를 실행시키고 uncaughtS를 넘겨줍니다
TryMap또한 Publisher이기에 subscribe()메서드를 실행시키면 receive(subscriber:_)를 연쇄적으로 실행시킵니다
그렇게되면 결국 TryMap의 receive(subscriber:uncaughtS)라는 메서드가 실행됩니다
TryMap도 TryMap.Inner객체를 만들어줍니다, downstream으로는 Catch.Inner.UncaughS를 가지고 있고 map에는 trymap의 클로저를 가지고 있게되겠죠
그림으로 표현해보면 위와같은 그림이 되겠네요 ㅎㅎ…아주아주 복잡한 형태의 TryMap.Inner가 완성되었습니다
TryMap의 upstream의 subscriber()를 실행시키고 TryMap.Inner를 전달해주니 당연히 receive(subscrbier:_)가 연쇄적으로 호출되고 TryMap.Inner가 전달될겁니다
passthroughSubject의 receive(subscriber:_)메서드에서는 구독을 위한 구독권인 conduit을 만들게되고 conduit의 parent에는 passthroughSubject가 들어가고 downstream에는 TryMap.Inner가 들어갑니다
이제부턴 operator를 지나서 sink까지 conduit을 전달하는 과정이 되겠죠?
subscriber의 receive(subscription:)메서드가 실행되는데 여기선 subscriber는 TryMap.Inner니까 TryMap.Inner의 receive(subscription:)의 구현부를 살펴보겟습니다
TryMap도 operator이기때문에 단순히 downstream에게 conduit을 전달해줍니다
TryMap.Inner의 downStream은 Catch.Inner.UncaughtS니까 이번에는 Catch.Inner.UncaughtS의 receive(subscription:_)을 한번 확인해보겠습니다
Catch.Inner.UncaughtS의 receive(subscription:)은 inner인 Catch.Inner의 receivePre(subscription:)메서드를 실행시킵니다
그러면 Catch.Inner의 State가 .pre라는 type으로 바뀌게되고 연관값으로 conduit을 가지고 있게됩니다
이건 제 개인적인 생각인데 catch라는 operator가 우선 error가 없다고 가정하고 무조건 값을 pre(미리)받는다고 가정하기때문에 이렇게 enum중에서도 pre로 설정해놓는게 아닐까 싶네요(어디까지나 개인적인 소견입니다)
그리고 Catch.Inner의 downstream인 sink에게 subscription을 넘겨주고 sink가 conduit을 받아 stream이 연결된상태로 바뀌게되면서 send이전의 과정이 마무리되게됩니다
원래는 하나의 포스팅으로 마무리할수있을줄알았는데 정리하다보니 양이 너무 방대해져서 두번으로 나눠서 업로드하는게 좋을것같겠다는 생각이듭니다!
아마 코드를따라오면 이 Inner가 저 Inner같고... 뭐가뭘계속 호출은 하는데 뭐가뭔지 모르겠고... 당연히 그럴겁니다! 저도 사실 같은 코드의 호출을 40번넘게 따라가서 이제야 좀 흐름이 보이는 그런 상황이거든요
그래도 한가지 말씀드리고 싶은건 몰라도 괜찮다는겁니다! 다음 포스팅에서 정말 상세한 호출순서를 몰라도 catch에 대한 전체적인 그림을 그리실수있게 제가 이해한 큰그림으로 다시 요약 설명해드리겠습니다
그럼 다음 포스팅에서 뵙겠습니다!