티스토리 뷰

반응형

WWDC 2019에서 Combine이 발표되었다.
Combine은 Rx와 똑같다! 라고 말하던데 정말 똑같을까..? 🤔

Rx와 Combine을 비교해보자

 

1. 스펙 비교 

 

https://github.com/freak4pc/rxswift-to-combine-cheatsheet

 

Rx와 Combine은 모두 Reactive 프로그래밍을 위한 framework이다 

하지만 Rx는 iOS 8이상부터, Combine은 iOS 13이상부터 사용할 수 있다
(그래서 Combine을 실제 프로덕트에서 쓰기까지는 최소 1년에서 최대 3년까지 걸릴 것이라고 말한다) 

 

Rx는 Third party framework인 반면 Combine은 애플에서 만든 buit-in framework이다...! 

그리고 Rx는 Rxcocoa와 Combine은 SwiftUI와 UIBinding을 하도록 설계되어졌다

(하지만 Combine은 SwitUI와만 쓸수있고 UIKit과 못쓴다 이런 개념은 아니다! 뒤에 가서 설명하겠음)

2. 성능 비교 

어떤 분이 0부터 천만까지 있는 sequence를 연산할때 Rx와 Combine의 성능이 어떤지 비교해보는 실험을 해주셨다. 

 

 

 

 

 

 

결과는 Combine이 시간이나 메모리 할당면에서 모두 성능이 더 좋았다

 

 

이 실험 내용이 있는 아티클은 여기서 볼 수 있다 

https://medium.com/flawless-app-stories/will-combine-kill-rxswift-64780a150d89 

 

Will Combine kill RxSwift?

Combine framework was presented at WWDC2019. Let’s talk about whether something changed in our lives.

medium.com

 

3. 기본 개념 비교 

  • 1. Observable Publisher

  • 2. Subject

  • 3. Change Thread 

  • 4. Disposable Cancellable

  • 5. Operators

이렇게 다섯 가지 개념을 비교 해볼 것이다

 

3.1 Observable과 Publisher 

Rx의 Observable = Combine의 Publisher

Rx의 Observer = Combine의 Subscriber 

 

하지만 

Publisher는 프로토콜이고 AnyPublisher는 Publisher을 따르는 struct이다 

Subscriber는 프로토콜이고 AnySubscriber는 Subscriber를 따르는 struct이다

 

그러니까 Observable은 AnyPublisher와 같고 Observer는 AnySubscriber와 같다고 말하는 것이 정확하다 

 

Observable과 Publisher의 차이가 있다면, Publisher는 만들 때 데이터 타입 뿐 만 아니라 에러 타입까지 지정해주어야한다는 것이다 

 

 

 

이렇게 Observable은 데이터 타입만 지정해주면 된다 

 

 

AnyPublisher<String, APIError>

AnyPublisher를 만들 때는 데이터 타입과 에러타입지정해주어야한다.

기본 Error 타입으로 지정해줄 수 있고 APIError 같은 커스텀 에러를 만들어서 지정해줄 수 도 있다 

 

3.2 Subject

 Rx와 Combine 모두 개념도 이름도 같은 Subject라는 녀석이 있다 

 

Rx의 Subject 

- Observable 과 Observer 모두로 동작한다 

- 구독자들에게 value, error, completed 를 준다

- PublishSubject (initial value 없음) / BehaviorSubject (initial value있음)

 

 

Combine의 Subject 

- Publisher와 Subscriber 모두로 동작한다 

- 구독자들에게 value, error, finished를 준다 

- PassthroughSubject (initial value 없음) / CurrentValueSubject (initial value있음)

 

 

완료이벤트를 Rx에서는 completed, Combine에서는 finished라고 부르는 것빼고는 정말 다 똑같다...!!! 

initial value가 있는 Subject, 없는 Subject로 나눠놓은 것도 똑같다...!! 

 

하지만 BehaviorSubject랑 CurrentValueSubject 는 조금 다른 점이 있다

BehaviorSubject는 subscriber가 구독한 시점에 초기값 또는 최신값(초기값 이후 발행된 값이 있다면)을 준다

CurrentValueSubject는 subscriber가 구독한 시점에 초기값을 준다 

 

밑의 코드를 보면서 Rx의 Subject와 Combine의 Subject를 비교하여보자 :-) 

 

1) RxSwift의 PublishSubject / Combine의 PassthroughSubject 

 

 

 

 

 

 

두개 다 초기값이 없고 

구독자들에게 value를 주고 completed를 주고 그 이후에 스트림이 끝나서 "Is Alive?" 가 출력되지 않음을 볼 수 있다 

 

 

2) Rx의 BehaviorSubject / Combine의 CurrentValueSubject

 

 

 

BehaviorSubject는 Init Value라는 초기값이 있고 그 이후에 "Is anyone listening?" 을 방출했다 (이 값이 최신값..!)

구독자들이 구독한 시점에 받은 값을 보면 "Is anyone listening?" 을 받았음을 알 수 있다 

 

BehaviorSubject는 구독자들이 구독한 시점에 초기값을 주거나 초기값 이후 발행된 값이 있으면 최신값을 주기 때문이다 

 

 

 

 

하지만 똑같은 상황에서 CurrentValueSubject를 구독한 구독자들이 구독한 시점에 받은 값을 보면 "Is anyone listening?" 이 아닌 "Init Value" 이다

 

CurrentValueSubject는 구독자들이 구독한 시점에 초기값을 주기 때문이다 이 점이 BehaviorSubject 와 다르다 

그 외에 구독자들이 value를 받는 것이나 completed 또는 finished 이후에 스트림이 종료되는 것은 두 Subject 모두 똑같다 

 

3.3 Change Thread 

멀티쓰레딩 관련해서도 비교해보자

 

Rx에서 연산이나 구독 같은 특정 작업의 쓰레드를 지정해줄 때 observeOn 을 쓴다 

Combine에서도 이와 같은 용도인 receiveOn(on:) 이 있다 

 

그리고 Rx에서 Observer가 동작할 쓰레드를 지정해줄 때 SubscribeOn을 쓴다 

Combine에서도 이와 같은 용도인, 이름도 같은 Subscribe(on:) 이 있다 

 

하지만 Rx의 SubscribeOn은 위치에 상관없이 쓸 수 있다면 Combine의 Subscribe(on:)은 위치에 상관있다는 차이점이 있다

이 차이점은 뒤에 가서 코드와 함께 살펴보기로 하고 우선 Rx와 Combine에서 Thread바꾸는 것을 어떤 식으로 쓰는 지 부터 살펴보자 : )

 

 

우선 Rx를 살펴보자 

 

 

 

 

Observable이 동작할 쓰레드를 subscribeOn으로 지정해주었다

그리고 observeOn으로 특정 작업의 쓰레드를 지정해주었다 (observeOn works on downstream!)

(API에서 데이터가져오는 작업은 background에서 하시오 구독은 Main에서 해서 view의 visible여부 바꿔주는 작업을 하시오 하고 있는 것이다)

 

그 다음 Combine을 살펴보자 

 

 

 

 

Publisher가 동작할 쓰레드를 subscribe(on:) 으로 지정해주었다 

그리고 receiveOn으로 특정 작업의 쓰레드를 지정해주었다 (receiveOn works on downstream!)

 

역시 사용하는 방식도 똑같다...! 

 

이제 차이점이 있다고 말했던 Rx의 SubscribeOn과 Combine의 Subsribe(on:) 을 샅샅이 살펴보자_!! 

 

우선 Rx의 SubscribeOn에 대해서 명확히 다시 짚어보자 

Rx의 SubscribeOn은 전체 체인에서 어디에 위치하든지 상관없이 Observable의 쓰레드를 바꿀 수 있는 녀석(?) 이다 

 

http://minsone.github.io/programming/reactive-swift-observeon-subscribeon

이 그림을 보면 이해하기 쉬운데 subscribeOn이 밑에 있음에도 불구하고 Observable을 파란색으로 바꾸는 것을 볼 수 있다

그리고 observeOn이 downStream을 바꾸므로 주황색, 분홍색 으로 체인의 색깔이 바뀌는 것이다 

 

실제 코드로 봐보자 

 

 

 

 

Observable 바로 밑에 subscribeOn을 위치시켜주지 않아도 쓰레드가 잘 바뀌는 것을 볼 수 있다

즉 Rx에서는 SubscribeOn을 위치상관없이 사용해도 된다 

 

 

그럼 Combine에서는 어떨까?

 

 

 

Rx와 똑같이 코드를 작성하였지만 예상대로 쓰레드가 바뀌지 않는 것을 볼 수 있다...! 

 

 

 

 

이렇게 Publisher 바로 뒤에 Publisher가 동작할 스레드를 지정해주어야지 결과가 예상대로 나온다 

즉 Combine에서는 SubscribeOn을 위치상관없이 사용하면 안된다 

 

 

왜 두 개가 다른 것일까?! 

애플 문서에 보면  "subscribe(on:) changes the execution context of upstream messages." 라고 나오는데

Rx의 subscribeOn은 upstream & downstream을 모두 바꾸기 때문이다 

 

정리를 하면...!! 

 

RxSwift 

  • ObserveOn = It works only downstream
  • SubscribeOn = It works upstream & downstream 

 

Combine

  • receiveOn(on: ) = It works only downstream
  • Subscribe(on:) = It works only upstream

 

 

3.4 Disposable과 Cancellable 

Rx에는 Disposable과 DisposeBag이라는 개념이 있다

DisposeBag에 스트림을 담으면 deinit 될때 자동으로 dispose 된다 

 

Combine에도 Disposable과 비슷한 개념인 AnyCancellable이 있다. deinit 될때 자동으로 cancel 된다

하지만 Rx의 disposebag 같은 개념은 없다 

 

 

우선 AnyCancellable이 정말 deinit 될때 자동으로 cancel 되는지 부터 살펴보자 :-)

 

 

<간단한 실험>

5 후에 이벤트를 발행하는 Publisher 3 후에 deinit되는 viewController가 있다 

viewController deinit 되면 viewController 안에 있는 Publisher cancel 것인가?! 

 

1) AnyCancellable에 넣어주지 않았을 때

 

 

 

deinit 되어도 stream이 계속 살아있다...! 

 

 

2) AnyCancellable에 넣어줬을 때 

 

 

 

deinit되면 구독의 관계도 같이 끊김을 볼 수 있다 

 

 

AnyCancellable을 사용하면서 아쉬운 점이 두가지가 있었다 

 

일단 DisposeBag과 대응하는 개념이 없다는 것....! 

 

Disposebag이 없으니 

var cancellables: [AnyCancellable] = []  

bag과 비슷한 개념인 cancellables을 만들어서 스트림을 계속 여기에 담아주며 써야했다 

 

결국 Combine은 CancelBag이 필요하지 않습니까????!!! 하는 아티클도 몇개 보았고 

 

실제로 Combine의 disposeBag이라는 CancelBag 라이브러리도 나왔다

https://github.com/devxoul/CancelBag 

 

devxoul/CancelBag

A DisposeBag for Combine. Contribute to devxoul/CancelBag development by creating an account on GitHub.

github.com

 

이런 불편함은 두번째 문제이고 사실 가장 큰 문제는 실수할 위험이 있다는 것이다...!! 

이게 무슨말이냐면... 

 

밑의 코드는 내가 실제로 프로젝트하면서 AnyCancellable을 썼던 코드인데 

(Combine의 assign은 Rx의 bind , Combine의 sink는 rx의 subscribe 와 비슷한 개념)

 

assign과 달리 sink를 한 stream은 AnyCancellable로 wrap해서 넣어주고 있음을 볼 수 있다 

 

 

 

 

 

왜 그러냐면...! 

public protocol Cancellable

final public class AnyCancellable : Cancellable 

Cancellable은 프로토콜이고 AnyCancellable은 Cancellable을 따르는 클래스이다 

 

assign은 AnyCancellable을 리턴하지만

sink는 Subscribers.Sink<Self> 를 리턴하는데

final public class Sink<Upstream> : Subscriber, Cancellable

이 Sink는 Cancellable 프로토콜을 따르고 있다

그래서 assign과 달리 sink는 AnyCancellable로 wrap해줘야하는 것이다...! 

 

처음에 이것을 잘 모르고 썼을 때는 assign한 것도, sink한 것도 모두 Cancellable형 배열에 담기니까 

Cancellable형 배열로 메모리관리를 해주었다 

 

 

 

하지만!!! 이렇게 하면 안된다 AnyCancellable과 달리 Cancellable은 deinit될 때 auto-cancel이 안되기 때문이다...!! 

 

애플 개발자 포럼을 보면 나같은 실수를 한 사람의 글을 볼 수 있다 

demo 영상에서 말한 것과 다르게 Cancellable이 왜 deinit 될 때 자동으로 cancel 안되는 것인가요?! 라는 질문이였는데, 답변으로 sink한 결과를 AnyCancellable로 wrap하세요~ 라는 코멘트가 달렸다 

 

 Apple developer forum

 

 

앞으로 더 많은 사람들의 실수가 예상된다 😭😭😭 

 

이와 달리 disposebag은 bind를 하든 subscribe를 하든 다 disposebag에 담겨서 좀 더 편하고 실수할 위험도 적은 것 같다 Disposebag 짱짱.. 

 

 

 

 

 

 

그 다음 또 아쉬운 점은 Combine은 cancel 여부를 확인할 블럭도 없다는 것이였다... 

 

이렇게 Rx는 dispose여부를 확인할 수 있는 블럭이 예쁘게 있는데

 

 

 

 

Combine에는 없다 😭

 

 

 

 

 

----------------------------- 추가 ---------------------------

이런 메소드가 추가되었습니다!! 

이제 이런 식으로 사용하면 됩니다--!! 
밑의 사진은 렛츠스위프트에서 뱅.샐의 보영님이 Rx의 DisposeBag과 Combine의 Set<AnyCancellable> 를 비교해주신 슬라이드입니다

 

https://www.slideshare.net/BoYoungPark11/rxswift-to-combine-192620911

 

3.5 Operators

 

Rx와 Combine의 Operator들은 거의 비슷하다 

예를 들어 Convenience Operator를 봐보자 

 

 

이렇게 Just 연산자도 같고 

 

 

 

 

 

 

Sequence 만드는 연산자도 같다 

 

 

 

 

 

Combine Operator는 조금 다르다....! 

Rx는 연산하는 observable 갯수와 상관없이 Operator가 하나 이다

하지만 Combine은 연산하는 publisher 갯수에 따라서 Operator가 여러개 이다 

 

예를들어 Rx는 CombineLatest, Merge 이렇게 Operator가 있다면 

Combine은 두개는 CombineLatest, 세 개는 CombineLatest3, 네 개는 CombineLatest4 이렇게 있고 

두 개는 Merge, 세 개는 Merge3, 네 개는 Merge4 ...... 이런식으로 있다 

 

Apple Combine Framework


그래서 실제 작업을 할 때  5개의 Publisher를 CombineLatest로 연산해야하는 상황이 있었는데,

combineLatest4 까지 있고 combineLatestMany는 없어서 

4개를 먼저 연산하고 그 publisher를 다시 마지막 하나랑 연산하고... 이렇게 두번 연산해야하는 조금의 불편함이 있었다.. 😫

 

 

 

 

 

 

그리고 Combine에만 있는 try + operator 도 살펴보도록 하자 

 

Combine은 Rx와 달리 에러를 던질 수 있는 Operator도 제공한다 

Combine은 throwing operator와 not throwing operator를 나눠놓았다 

모든 operator가 다 그런 것이 아니라 몇개의 operator 들만 try 붙은 (throwing) / try 안붙은 (not throwing) 버전이 있다

 

몇 가지 Operator들을 예시로 가져왔다 : )

 

 

 

그 중 map과 trymap을 한번 살펴보자 

 

map은 이렇게 생긴 not throwing opertor 이다 

public func map<T>(_ transform: (Output) -> T) -> Publishers.Just<T>

 

trymap은 이렇게 생긴 throwing opertor 이다 

public func tryMap<T>(_ transform: (Output) throws -> T) -> Publishers.Once<T, Error>

 

에러 처리를 안에서 해서 반드시 결과를 리턴해줘야하는 map과 달리 

 

 

 

 

 

trymap은 안에서 에러를 던질 수 있다. catch 로 체이닝을 하여 에러를 밖에서 처리해주었다 

 

 

 

 

 

map에도 catch를 체이닝해보았는데 Error타입이 아니라 Never(에러없다) 타입을 받는다 

catch { (never: Never) in

} 

 

 

Operator 비교는 rxswift - to - combine cheatSheet  을 참고하시면 좋습니다 👍

반응형
댓글