Customize handling of asynchronous events by combining event-processing operators.

Swift Combine 프레임워크는 위의 인용구에 나와있다시피 비동기 작업을 용이하게 처리하기 위한 프레임워크로, Reactive Extension과 같은 라이브러리를 거의 완전히 대체할 수 있다(애시당초 대체하라고 나왔다고 볼 수 있다). Combine이나 Reacive Extension과 같은 형태를 일반적으로 Reactive 혹은 반응형 프로그래밍이라고 부른다. 이게 명칭때문에 React나 반응형 웹 디자인과 개념을 헷갈려하는 사람이 종종 보이는데, 전혀 다른 개념이다.

여튼 이런 반응형 프로그래밍의 장점이자 특징이라고 한다면 바로 연산자 호출을 통한 작업 스레드 전환이다. 그리고 이 특징이 비동기 작업을 용이하게 만들어주는 것이기도 하고. 그렇다면 Combine 프레임워크에서는 어떻게 스케쥴러를 통해 스레드 전환을 할 수 있는지, 그리고 스케쥴러를 활용한 다른 기능은 뭐가 있는지 알아보자.


NOTE

참고로 스케쥴러와 스레드는 서로 다른 객체이다. 간단히 설명하자면, 스케쥴러가 구현된 세부사항에 따라 적절한 스레드를 사용하도록 지시하는 것과 비슷하다. 실제로 자세히 들어가면스케쥴러의 구현체 각각이 스레드를 어떤 식으로 사용하는지가 다 다르다.


subscribe(on:)receive(on:)

subscribe(on:)receive(on:)은 위에서 언급한 스케쥴러를 통한 스레드 전환을 수행하는 연산자다. 둘의 동작 차이는 메서드명을 간단하게 문장으로 풀이해서 번역해보면 느낌이 올 것이다.

  • subscribe(on scheduler: S) > subscribe on scheduler S > 스케쥴러 S에서 구독
  • receive(on scheduler: S) > receive on scheduler S > 스케쥴러 S에서 수신(혹은 수령?)

어떤가 느낌이 좀 오는가? 여기서 Combine Scheduler 프로토콜의 정의인 ‘클로져를 언제 어떻게 실행할지 정의하는 프로토콜’을 생각하면, subscribe(on:)은 구독하려는 클로져에 대해 스케쥴러를 지정하는 것이고, receive(on:)은 수신하는 클로져에 대해 스케쥴러를 지정하는 것이라고 볼 수 있다. 물론 이것만으로는 아직 이해가 어려울 수 있다. 그렇다면 쉬운것부터 생각해보자.

수신하는 클로져란 무엇일까? Combine은 데이터 그 자체보다는 데이터의 흐름을 다루는 프레임워크다. 이런 관점에서 수신하는 클로져란 무엇일지 생각해보면, 데이터의 흐름을 받아 뭔가 조작을 가하거나 하는 클로져를 말한다는 것을 알수 있다. 예를 들어, .map(_:)과 같은 연산자를 생각해보자. 이 친구의 경우에는 클로져를 인자로 받아 그 클로져에서 데이터 흐름을 통해 흘러들어온 데이터에 조작을 가한후 다음 흐름으로 보내주는 역할을 한다. 그렇다. 이 .map(_:) 메서드가 받은 클로져가 바로 수신하는 클로져다. 고로 receive(on:) 은 이런 map과 같은 메서드에 인자로 넘어가는 클로져를 언제/어떻게 실행할지 정의해놓은 스케쥴러를 지정하는 거라고 볼 수 있다.

그렇다면 구독하는 클로져란 무엇인가? 구독이 발생하는 지점의 클로져를 말한다. 예를 들면 다음과 같다.

func doSubscribe() { // Closure 0
    SomePublisher()
        .handleEvents(receiveSubscription: { _ in // Closure 1
            // 여기서 스레드를 체크하면 Closure 0과 동일한 스레드가 나온다.
        })
        .sink(/* ... */)
}

doSubscribe 함수에서 SomePublisher를 구독하고 있다. 이때 이 구독하는 이벤트가 발생하는 doSubscribe의 몸체 클로져가 바로 이 구독하는 클로져이다. 이 때 doSubscribe가 메인 스레드에서 수행된다면 구독 역시 메인 스레드에서 수행된다. 하지만 아래처럼 별도의 subscribe(on:) 을 호출해 global 큐를 사용하도록 한다면?

func doSubscribe() { // Closure 0
    SomePublisher()
        .subscribe(on: DispatchQueue.global())
        .handleEvents(receiveSubscription: { _ in // Closure 1
            // doSubscribe가 어느 스레드에서 수행되었는지는 몰라도 적어도 여기서는 백그라운드 스레드에서 수행된다.
        })
        .sink(/* ... */)
}

doSubscribe가 어느 스레드에서 수행되었는지와 상관없이, 구독 이벤트가 발생하는 시점의 스레드는 DispatchQueue.global() 에 의해 수행 시점의 적절한 백그라운드 스레드로 전환되는 것이다.

참고할 것은 subscribe(on:)은 업스트림에 영향을 미치기 때문에 2회 이상 호출되었을 경우 근원이 되는 업스트림에 가까운 메서드에 인자로 넘김 스케쥴러의 명령을 따르게 된다.

그렇다면 이 인자로 넘기는 스케쥴러는 무엇이고 또 어떤게 있으며, 어떤 상황에 어떤 스케쥴러를 사용해야 하는걸까? 이에 대해서는 좀 더 정리해서 다음 포스팅에서 얘기해보도록 하겠다(언젠간…).