RxSwift

[RxSwift] Observable stream 에서 share가 필요한 이유 | multiCast, uniCast

하이D:) 2024. 8. 23. 11:27

 

RxSwift는 사용하다 보면 참 편리한 아이인 것 같다. 반응형으로 사용자의 이벤트를 구독하고 방출된 이벤트를 관찰하고 있다가 처리해주다 보니 MVVM 패턴과도 찰떡인 게 사용하면서 체감된다.

 

하지만 우리가 이벤트를 방출할 때 사용해주는 Observable이 편리하긴 하지만 여러 곳에서 구독을 할 경우 유의해주어야 할 점이 있는데 이것에 대해 알아보자!

 

ReactiveX를 공부할 때 반드시 알아야할 multicast, unicast 개념과도 연결되니 꼭 알고 넘어가기로!

 

 

 

 

 

 

 

📍 Observable Stream

 

우리는 이미 Observable를 여러번 구독할 수 있다는 것을 알고 있는데... 근데 이렇게 여러번 구독하게 되면 각각 구독하는 시점에 Observable stream이 새롭게 생긴다. 즉, 동일한 Observable stream이 그대로 전달되는 게 아니라는 것이다.

 

 

 

근데 이게 무슨 문제가 있어서 유의해주어야하는 걸까? Observable stream은 우리 생각보다 중요하다! 왜냐!

  1. Observable stream으로인해 코드의 흐름에 따라 방출된 이벤트가 변화될 가능성이 있고,
  2. 무거운 작업(예를들어 네트워킹!)이 이 Observable steam에서 실행될 경우 구독 시점마다 새로 실행될 가능성이 있기 때문에 의도치 않게 api를 중복 호출 하거나 불필요하게 앱의 리소스를 사용하게 될 수도 있다!!

 

 

사실 나도 이게 왜 중요한 개념인지 몰랐는데 내 코드에서 발생한 문제점 때문에 Observable stream이 공유되는지 혹은 안되는지의 여부가 얼마나 중요한지 알게 되었다 ㅎㅎ ( 나도 알고 싶지 않았써…)
그래도  🌱새싹 🌱 세션 중에 Observable stream에 이러한 특성이 있고 Observable stream이 공유되게 하려면 어떻게 해야 하는지에 대한 세션을 들은 바로 그날, 문제가 될 수 있는 상황을 맞닥뜨려버려서 럭키!! 이런 키워드 몰랐으면 왜 이런 문제가 발생하는지 한참 헤맸을 것 같다.

 

 

 

 

 

 

 

 

📍문제가 될 수 있는 상황

 

1️⃣

 

아래 코드를 보면 이벤트 발생하는곳은 test 한 곳인데 구독(bind)을 세 군데에서 하고 있다.

 

그냥 뇌피셜을 돌려보면 test 한 군데에 이벤트발생에 대한 Observable stream을 정의해 두었기 때문에 한번 정의된 Observable stream이 모든 구독에서 사용되는거 아니야?라고 생각할 수 있다. ( 당연히 나도 그렇게 생각했고 ㅎㅎ)

 

but, 위에서 말한 대로 Observable의 특성상 Observable stream이 공유되지 않아, 구독(subscribe, bind)를 할 때마다 새로운 스트림이 생기기 때문에 하나의 이벤트 발생을 상수에 할당하여 정의해두었다고 하더라도 구독(bind)할 때마다 새로운 스트림이 생겨서 프린트가 이렇게 나오는 것이다.

let test = nextButton.rx.tap
    .map{"안녕하세요 \(Int.random(in: 1...100))"}
//이벤트 발생하는곳은 한 곳인데 구독을 세 군데에서 하고 있음

test
    .bind(onNext: { text in
        print("1 --> ", text)
    })
    .disposed(by: disposeBag)

test
    .bind(onNext: { text in
        print("2 --> ", text)
    })
    .disposed(by: disposeBag)

test
    .bind(onNext: { text in
        print("3 --> ", text)
    })
    .disposed(by: disposeBag)

 

 

 

 

 

 

2️⃣ 네트워킹

 

네트워킹 통신의 결과가 unicast(이벤트 스트림이 공유되지 않음)로 반환될 경우에 여러 관찰자를 통해 구독할 경우 api를 중복으로 호출하는 문제점이 발생할 수 있다

    func getBoxOfficeData(dateString : String) -> Observable<BoxOffice> {
        
        let url = "https://kobis.or.kr/kobisopenapi/webservice/rest/boxoffice/searchDailyBoxOfficeList.json?key=\(koficKey)&targetDt=\(dateString)"
        
        let boxofficeResult = Observable<BoxOffice>.create { observer in
            guard let url = URL(string: url) else {
                observer.onError(APIError.invalidUrl)
                return Disposables.create()
            }
            
            URLSession.shared.dataTask(with: url) { data, response, error in
                
                //error control code ...
                
                if let data = data, let appdata = try? JSONDecoder().decode(BoxOffice.self, from: data) {
                    observer.onNext(appdata)
                    observer.onCompleted()
                }
            }
            .resume()
            
            return Disposables.create()
        }
            .debug("💜boxofficeResult💜")
        
        
        return boxofficeResult
    }

 

let boxOfficeObservable : Observable<BoxOffice> = searchBar.rx.searchButtonClicked
    .debug("-- debug --")
    .map{ "20240707" }
    .flatMap{ query in
        NetworkManager.shared.getBoxOfficeData(dateString: query)
        
    }

boxOfficeObservable
    .subscribe(with: self){ owner, data in
        print("subscribe 1️⃣")
    }
    .disposed(by: disposeBag)

boxOfficeObservable
    .subscribe(with: self){ owner, data in
        print("subscribe 2️⃣")
    }
    .disposed(by: disposeBag)

boxOfficeObservable
    .subscribe(with: self){ owner, data in
        print("subscribe 3️⃣")
    }
    .disposed(by: disposeBag)

 

 

 

 

 

 

 

 

 

 

 

📍multiCast, uniCast

위에 살짝 uniCast라는 것을 언급하기도 했는데 

multiCast와 uniCast는 이름에서도 유추할 수 있듯이 상반되는 개념이다.

 

 

- multiCast : 이벤트 스트림을 공유

- uniCast : 이벤트 스트림을 공유하지 않음

 

 

위에서 스트림에 대해서 어느정도 살펴봤으니 감이 오죠?

 

ReactiveX를 공부할 때 반드시 알아야 하는 개념인 Subject와 Observable도 이 지점에서 차이가 있는데, Subject는 multiCast특성을 가지고 , Observable은 uniCast특성을 가진다. 그래서 지금까지 이벤트 스트림이 공유되지 않는 uniCast에 대한 예시를 들 때 모두 Observable이었던 것! (이건 다음에 쓸 Hot Observable, Cold Observable 관련한 글과도 연관되는 내용이니 잘 인지하고 넘어가자!)

 

 

 

 

 

위에서 살펴본 문제 상황에서 모든 구독 상황에 새로운 스트림이 만들어지도록 의도가 된 것 이면 이러한 상황이 문제가 없겠지만 같은 Observable stream을 공유할 수 있도록 하고 싶은데 이런 코드의 실행이라면 문제가 있을 것이다.

그렇다면, unicast 인 Observable을 multi cast로 바꿀 수 있을까??

 

 

 

 

 

 

 

 

 

 

 

 

📍uniCast -> multiCast

 

 

1️⃣ PublishSubject 활용

첫 번째 방법은 PublishSubject에 바인딩시킨 다음 그 PublishSubject를 사용해 주는 방법이다.

PublishSubject는 Observable과 달리 이벤트 스트림을 공유하는 multicast 특성을 가졌기 때문에 PublishSubject를 여러 번 구독하더라도 하나의 stream에서 방출된 이벤트가 저장되었다가 Observer가 동일한 이벤트를 처리할 수 있게 된다.

 

(다음 글에 쓸 예정인 Hot Observable과도 연결되는 내용이니 여기서 Subject은 Hot Observable이기 때문에 multicast 특성을 가졌다고 이해하고 넘어가 보자! )

 

 

let boxOfficeSubject = PublishSubject<BoxOffice>() //⭐️

searchBar.rx.searchButtonClicked
    .debug("-- debug --")
    .map{ "20240707" }
    .flatMap{ query in
        NetworkManager.shared.getBoxOfficeData(dateString: query)
        
    }
    .subscribe(onNext : { boxOffice in
        boxOfficeSubject.onNext(boxOffice)  //⭐️
    })
    .disposed(by: disposeBag)

boxOfficeSubject
    .subscribe(with: self){ owner, data in
        print("subscribe 1️⃣")
    }
    .disposed(by: disposeBag)

boxOfficeSubject
    .subscribe(with: self){ owner, data in
        print("subscribe 2️⃣")
    }
    .disposed(by: disposeBag)

boxOfficeSubject
    .subscribe(with: self){ owner, data in
        print("subscribe 3️⃣")
    }
    .disposed(by: disposeBag)

 

 

 

 

 

 

2️⃣ .share

 

그리고 가장 간단한 방법으로는 share 연산자를 사용할 수 있다.

이렇게 하면 boxOfficeObservable을 여러 번 구독하더라도 하나의 이벤트 스트림을 공유한다!

let boxOfficeObservable : Observable<BoxOffice> = searchBar.rx.searchButtonClicked
    .debug("-- debug --")
    .map{ "20240707" }
    .flatMap{ query in
        NetworkManager.shared.getBoxOfficeData(dateString: query)
        
    }
    .share()

 

 

 

 

 

 

3️⃣ .asDriver

 

asDriver 연산자를 사용해서 Driver로서 처리해 주는 방법도 있다. Driver는 ReactiveX에서 제공하는 traits 중에 하나인데 모른다면 문서 읽어보는 것을 추천! 

https://github.com/ReactiveX/RxSwift/blob/main/Documentation/Traits.md#driver

 

 

 

간단히 요약하면

1) 이벤트의 처리가 mainThread에서 일어나서 UI를 변경/업데이트할 경우 유용하고,

2) 에러를 방출하지 않으며,

3) 그리고 이벤트 스트림을 공유한다.

 

 

 

문서에서  asDriver(onErrorJustReturn: []) 이 코드 한 줄에 밑에 코드랑 동일하다고 설명한다.

let safeSequence = xs
  .observeOn(MainScheduler.instance)        // observe events on main scheduler
  .catchErrorJustReturn(onErrorJustReturn)  // can't error out
  .share(replay: 1, scope: .whileConnected) // side effects sharing

return Driver(raw: safeSequence)            // wrap it up

 

 

 

 

다시 본론으로 돌아와서 asDriver 연산자를 사용하여 Driver로 만들어주면 스트림을 공유하게 된다.

let boxOfficeObservable : Observable<BoxOffice> = searchBar.rx.searchButtonClicked
    .debug("-- debug --")
    .map{ "20240707" }
    .flatMap{ query in
        NetworkManager.shared.getBoxOfficeData(dateString: query)
        
    }
    .asDriver(onErrorJustReturn: /*에러발생시 방출할 반환값*/ )

 

 

 

 

 

 

 


 

 

 

이렇게 multiCast/uniCast와 Observable stream의 특, 그리고 unicast를 multicast로 바꾸어 처리해 줄 수 있는 방법들을 알아보았다.

 

개념 자체는 어렵지 않은데, 실제 사용할 때 multiCast, uniCast를 염두하고 코드를 짜야 의도치 않은 사이트이펙트를 생성하지 않는다는 걸 경험을 통해 체득한 기회였다 ㅎㅎ