Combine - subscribe(on:) vs receive(on:) 을 알아보자

파이집·2024년 1월 3일

Combine 이야기

목록 보기
1/1
post-thumbnail

RxSwift 와 Combine 을 하면서 공부는 해봤지만 실제 코드에는 적용하기 어려웠을 두 개의 operator 를 이번에 사용해보게 되면서 포스팅을 하기로 마음 먹었습니다. 이미 수많은 글에 이와 관련된 글이 많지만, 제가 직접 포스팅해보고 싶어서 하게 됐습니다.

결론부터 이야기하자면...

1. subscribe(on:) 은 upstream 에 영향

- upstream 에는, subscription, request, cancel 등이 있다.

2. receive(on:) 은 downstream 에 영향

- downstream 에는 value 와 complete 가 있다.


어떤 걸로 시험을 해볼까 하다가, 그냥 간단하고 언제나 사용하는 랜덤 숫자 뽑기 기능으로 비교를 해봤습니다.

subscribe(on:)

PassthroughSubject

로 먼저 진행을 하겠습니다.

먼저 Output 으로 받는 그 쓰레드 자체가 어딘지 궁금했습니다.
왜냐하면 보통 쓰레드 관련된 고민은 보통 Input 과 Output 의 쓰레드이기 때문이죠. 그 중간의 과정은 알아서 해주겠지 라는 생각이 아무래도 크죠. (다른 머리 아픈것도 많기 때문에...)

struct ThreadCheckView: View {
    
    @State var number: Int = 0
    private let generator = RandomNumberGenerator()
    
    var body: some View {
        VStack {
            Button("Generate") {
                generator.generateNewNumber()
            }
            .padding()
            
            Text("\(number)")
                .onReceive(generator.subject, perform: { data in
                    number = data
                })
            
            Button("Complete") {
                generator.completeGenerator()
            }
        }
    }
}

final class RandomNumberGenerator {
    
    private var subscriptions = Set<AnyCancellable>()
    let subject = PassthroughSubject<Int, Never>()
    
    init() {
        subject
            .subscribe(on: DispatchQueue.global())
            .sink { randomNumber in
                print("🔥 \(randomNumber) has been generated. With thread of \(Thread.current)")
            }
            .store(in: &subscriptions)
    }
    
    func generateNewNumber() {
        subject.send(Int.random(in: -10...10))
    }
    
    func completeGenerator() {
        subject.send(completion: .finished)
    }
}

결과를 보실까요??
우리는 subscribe 를 Main Thread 가 아닌 곳에서 했지만?
Receive 된 Thread 를 보면 Main Thread 이죠.

어라라!
이래서 공식 다큐에서 정의를 살펴보면...

Output 에 관련된 게 아니라, upstream 의 operation 에 관련된 것이라고 써져 있습니다.

⭐️ 아하 그렇다면 receive 를 할때의 operation 과는 관련이 없구나! 라는 걸 알 수 있죠


그렇다면? 내부에서 벌어지는 일들에 대해서 조금 더 뜯어봅시다. 과연 이 말처럼 중간 과정의 upstream 친구들은 이 말처럼 다른 쓰레드에서 돌아갈까요?
handleEvents() 메서드를 사용해보시죠

final class RandomNumberGenerator {
    
    private var subscriptions = Set<AnyCancellable>()
    private let subscriber: AnySubscriber<Int, Never>
    let subject = PassthroughSubject<Int, Never>()
    
    
    init() {
        subscriber = AnySubscriber(receiveSubscription: { subscription in
            print("------------------------\n📬 Subscription has been received. - Thread of \(Thread.current)")
            subscription.request(.unlimited)
        }, receiveValue: { value in
            print("☀️ \(value) has been received. - Thread of \(Thread.current)")
            return .none
        }, receiveCompletion: { _ in
            print("🖌️ Completed - Thread of \(Thread.current)\n-------------")
        })
        
        subject
            .handleEvents(receiveSubscription: { _ in
                print("-: Received Subscription - Thread of \(Thread.current)")
            }, receiveOutput: { _ in
                print("-:: Received Output - Thread of \(Thread.current)")
            }, receiveCompletion: { _ in
                print("-::: Received Completion - Thread of \(Thread.current)")
            }, receiveCancel: {
                print("-:::: Cancelled - Thread of \(Thread.current)")
            }, receiveRequest: { _ in
                print("-::::: Received Request - Thread of \(Thread.current)")
            })
            .subscribe(on: DispatchQueue.global())
            .subscribe(subscriber)
    }
    
    func generateNewNumber() {
        print("\n\(#function) called. Thread of \(Thread.current)")
        subject.send(Int.random(in: -10...10))
    }
    
    func completeGenerator() {
        print("\n\(#function) called. Thread of \(Thread.current)")
        subject.send(completion: .finished)
    }
}
  1. Subscription 을 받았을 때
  2. Output 을 받았을 때
  3. Complete 됐을 때
  4. Cancel 됐을 때
  5. Request 를 받았을 때

를 추적해보는 거죠.
그럼 결과는 ?

보시면 Complete 와 Output 을 받았을 때를 제외하곤 모두 Main 쓰레드가 아닌 다른 쓰레드에서 실행된다는 점을 볼 수 있죠.

여기서 중요한 점은 그러면 왜? 다른 downstream 은 Main 쓰레드에서 실행되죠?
그건 우리가 SwiftUI 의 View 내부에서 이 함수를 실행시키기 때문이죠.
이 SwiftUI 의 View 는 @MainActor 이기 때문에 기본적으로 Main 쓰레드에서 실행이 되죠!


오 좋아요 어느정도 이해가 되셨죠?
이제는 그러면 Cancel 일 때는 어떤지도 해봅시다.

func cancelSomeCancellable() {
	let someCancellable = [20].publisher
    	.handleEvents(receiveRequest:  { _ in
        	print("Received Cancel - \(Thread.current)")
        })
        .delay(for: 50, scheduler: DispatchQueue.main)
        .subscribe(on: DispatchQueue.global())
        .sink { someValue in
   		     print("Value: \(someValue)")
        }

	DispatchQueue.main.asyncAfter(deadline: .now()+2) {
    	someCancellable.cancel()
    }
}

간단하게 함수를 View 내부에 넣어봤습니다.
2초 뒤에 cancel 을 시켜버렸는데요.

네~ Cancel Operation 도 subscribe 에서 정한 쓰레드에서 발생하는 걸 볼 수 있죠!


receive(on:)

그럼면 이제 receive(on:) 에 대해서 알아보죠!

-> "receive elements from publisher" 라고 되어 있습니다. 바로 Output 과 complete 입니다. 아마 failure() 도 똑같이 여기서 적용될 겁니다.

기존의 함수에서 subscribe(on:)receive(on:) 을 바꿔보죠. 그리고 실행을 해보겠습니다. 결과는??

이제 Subscription 받는 것, Request 를 받는 것 등 upstream 에 관련된 것들은 모두 Main 쓰레드에서 실행되고,
우리가 원한 Output 과 Complete 는 다른 쓰레드에서 발생하는 것을 볼 수 있죠.

그러면 Cancel 은 어떨까요?

upstream 의 operation 이라는 걸 바로 알려줍니다. Main 쓰레드에서 실행이 됐군요!


그러면 이제 초기값이 있는 Subject 로 이어나가보겠습니다.

CurrentValueSubject

이렇게 Subject 의 종류를 바꿨구요.
각각 subscribe(on:), receive(on:) 에 각각 DispatchQueue.global 을 적용했습니다.

subcribe(on:) 일때


receive(on:) 일때


차이가 보이시나요??
기본적으로 위에서 본 대로 똑같이 작동되지만!
첫 초기값의 쓰레드는 조금 다르게 실행되는 걸 볼 수 있습니다.
Subscription 의 Thread 와 같은 Thread 에서 실행되는 것 같죠?

아마도..!
처음 Subscription 이 전달될 때 같은 쓰레드로 전달이 되는 거 같다.
(이거 한번 더 찾아봐서 포스팅을 써봐야겠다..!)


암튼 이렇게 해서 포스팅을 마무리합니다...
생각보다 이렇게 뜯어보는 데에 더 재미를 느끼고 있네요!

재밌따!

그리고 틀린 점 지적은 항상 환영합니다! 미리 감사합니다! 😊

profile
다박다박 위로 올라가는 iOS 개발자

0개의 댓글