Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schedulers #31

Open
simoniful opened this issue May 31, 2022 · 0 comments
Open

Schedulers #31

simoniful opened this issue May 31, 2022 · 0 comments
Labels

Comments

@simoniful
Copy link
Owner

simoniful commented May 31, 2022

Ch.15 Intro to Schedulers

지금까지 강의를 진행하면서 우리는 스케줄러가 실제로 무엇인지, 그리고 스레딩이나 동시성을 어떻게 처리하는지에 대한 설명을 피하면서 스케줄러를 키워드로서 사용해왔다. 이전 buffer, delaySubscription, interval 연산자와 같은 일종의 동시성/스레딩 영역을 암시적으로 활용하는 메서드를 사용했었다.

스케줄러가 코드 영역 아래에서 일종의 마법을 부린다고 느낄 수도 있지만, 스케줄러를 이해하기 전에 observeOn operator가 무엇인지 이해가 선행되어야 한다. 이번 챕터에서는 RxSwift 추상화가 왜 그렇게 강력한지, 그리고 왜 비동기 프로그래밍으로 작업하는 것이 locks나 queues을 사용하는 것보다 훨씬 나은지 배우게 될 것이다.

참고: 커스텀 스케줄러를 만드는 것은 챕터에 포함하고 있지는 않다. RxSwift, RxCocoa 및 RxBlocking이 제공하는 스케줄러와 이니셜라이저는 일반적으로 99%의 사례를 커버할 수 있도록 구성되었기에 항상 내장 스케줄러를 우선적으로 고려해보자.

What is a scheduler?

스케줄러를 사용하기에 앞서, 그들이 무엇인지, 그리고 무엇이 아닌지 이해하고 넘어가보자. 요약하자면, 스케줄러는 프로세스가 진행되는 컨텍스트이다. 이 컨텍스트는 스레드, dispatch queue 또는 유사한 엔티티 또는 OperationQueueScheduler 내부에서 사용되는 Operation로 여겨 질 수 있다.

다음은 스케줄러를 어떻게 사용할 수 있는지에 대한 좋은 예를 보여준다:

image

이 다이어그램에는 캐시 연산자의 개념이 있다. 관찰 가능한 것은 서버에 요청하고 일부 데이터를 검색한다. 이 데이터는 어딘가에 데이터를 저장하는 캐시라는 사용자 지정 연산자에 의해 처리되고. 그 후, 데이터는 다른 스케줄러의 모든 subscribers에게 전달되며, 메인 스레드 위에 있는 MainScheduler일 가능성이 높기에 UI가 문제 없이 업데이트 된다.

Scheduler의 명확한 의미 찾기

스케줄러에 대한 한 가지 일반적인 오해는 스레드와 같다고 생각하는 거다. 처음에는 논리적으로 보일 수 있다 - 결과적으로 스케줄러는 GCD의 디스패치 대기열과 비슷하게 작동하기 때문이다.

하지만 이건 절대적으로 잘못된 이해 방식이다. 권장되지 않지만 커스텀 스케줄러를 작성하는 경우, 동일한 스레드를 사용하여 여러 스케줄러를 만들거나 여러 스레드 위에 단일 스케줄러를 만들 수 있다. 이상하겠지만 구성 가능하다.

image

기억해야 할 건 스케줄러는 스레드가 아니며 스레드와 일대일 관계가 없다는 것이다. 항상 스케줄러가 스레드가 아닌 작업을 수행하는 컨텍스트를 확인해야한다. 이번 챕터의 뒷부분에서, 이 걸 이해하는 데 도움이 되는 몇 가지 좋은 예시가 제시 된다. 참고하도록 하자!

프로젝트 설정하기

이제 코드를 작성해보자. 이번 프로젝트에서는 macOS용 간단한 command- line tool을 만들 거다. command- line tool 로 진행하는 이유는 스레드와 동시성을 다루고 있기 때문에, 앱에서 만들 수 있는 시각적 요소(UI)보다 일반 텍스트 출력이 이해하기 쉽다.

1장 "Hello RxSwift"에 설명된 대로 이번 챕터의 스타터 프로젝트에 대한 CocoaPods 종속성을 설치해보자. 완료되면 작업 공간을 열고 빌드 및 실행하면 디버거 콘솔에 다음이 표시된다.

===== Schedulers =====
00s | [E] [dog] emitted on Main Thread
00s | [S] [dog] received on Main Thread

진행하기 전에 Utils.swift를 열고 dump()과 dumpingSubscription()의 구현을 살펴보자

첫 번째 메서드는 dump()는 [E] 접두사("Emitted")를 사용하여 do(onNext:) 연산자 안에 요소와 현재 스레드 정보를 나타낸다. 두 번째 dumpingSubscription()는 [S] 접두사를 사용하여 유사한 정보를 구독한다. 그것은 관찰 가능한 것을 구독하여 어떤 스레드에서 요소를 받는지 보여준다. 두 함수 모두 경과 시간을 보여주므로, 위의 00s는 "0초 경과"를 의미하고 있다.

이 기능은 콘솔에 정보를 인쇄하는 두 가지 방법을 강조한다:

  • 연산자 체인에 부작용을 주입할 수 있는 do(onNext:)를 사용한다(관찰 가능한 시퀀스를 변경하지 않는 "side effect" 작업을 수행).
  • 관찰 가능한 시퀀스를 구독하고 거기에서 인쇄.

이제 주어진 시간에 어떤 스레드를 사용하고 있는지 확인할 수 있으므로, observable들의 체인이 스케줄러 간에 전환하는 것이 얼마나 쉬운지 배울 준비가 되었다.

스케쥴러 바꿔보기

RxSwift에서 가장 중요한 것 중 하나는 이벤트를 생성하는 내부 프로세스에 의해 부과되는 제한을 제외하고는 제한 없이 언제든지 스케줄러를 전환할 수 있는 기능이다. 연산자가 요소를 수신하는 스케줄러를 제어할 수 있어야하는 이유는 다음과 같다.

  • 백그라운드 스케줄러에서 리소스가 많이드는 작업을 수행.
  • 리소스가 많이드는 작업이 직렬 또는 병렬로 발생하는지 제어.
  • UI 업데이트를 위해 기본 스레드에서 전달을 보장.

참고: 스케줄러를 전환할 수 있는 연산자를 사용할 때, 시퀀스가 전송하는 요소가 스레드로부터 안전한지 확인해야한다. RwSwift 자체는 Apple의 Dispatch 프레임워크처럼 작동해서, 데이터의 thread-safety에 관계없이 스케줄러/스레드를 전환할 수 있기 때문에 주의해야한다.

스케줄러가 어떻게 행동하는지 이해하기 위해, 과일을 제공하는 간단한 Observable을 만들어 Main.swift 하단에 다음 코드를 추가해 보자

let fruit = Observable<String>.create { observer in
    observer.onNext("[apple]")
    sleep(2)
    observer.onNext("[pineapple]")
    sleep(2)
    observer.onNext("[strawberry]")
    return Disposables.create()
}

해당 Observable에는 sleep()이 포함되어 있다. 이것은 실제 애플리케이션에서 일반적으로 볼 수 있는 것이 아니지만, 구독 및 관찰이 작동하는 방식을 이해하는 데 도움이 된다.
생성한 Observable을 구독하기 위해 다음 코드를 추가하자.

fruit
    .dump()
    .dumpingSubscription()
    .disposed(by: bag)

빌드하고 실행해보면 아래처럼 콘솔에 스레드 부분이 프린트 된 걸 확인할 수 있다.

===== Schedulers =====
00s | [E] [dog] emitted on Main Thread
00s | [S] [dog] received on Main Thread
00s | [E] [apple] emitted on Main Thread
00s | [S] [apple] received on Main Thread
02s | [E] [pineapple] emitted on Main Thread
02s | [S] [pineapple] received on Main Thread
04s | [E] [strawberry] emitted on Main Thread
04s | [S] [strawberry] received on Main Thread

원래 BehaviorSubject인 dog가 있고 그 뒤에 2초마다 과일들이 프린트 되었다.
현재 과일은 메인 쓰레드에서 생성되지만, 동시성을 위해 백그라운드 쓰레드로 옮겨 실행할 수 있다. 백그라운드 스레드에서 과일을 생성하려면 subscribeOn을 사용해야 한다.

subscribeOn 사용하기

경우에 따라 Observable 계산 코드가 실행되는 스케줄러를 변경할 수 있다. 구독 연산자의 코드가 아니라 실제로 Observable 이벤트를 내보내는 코드를 변경할 수 있다. 해당 계산 코드의 스케줄러를 설정하는 방법은 subscribeOn을 사용하는 거다. 언뜻 보기에는 직관에 어긋나는 이름처럼 들릴지 모르지만, 잠시 생각해보면 말이 되기 시작한다.

Observable을 실제로 관찰하고 싶을 때, 먼저 구독해야 한다. 이 건 원래 프로세스가 어디에서 일어날지 결정한다. subscribeOn이 호출되지 않으면, RxSwift는 자동으로 현재 스레드를 사용합니다:

image

해당 과정은 메인 스케줄러를 사용하여 메인 스레드에 이벤트를 만들고 있다. MainScheduler는 메인 스레드 위에 있기에, UI로 작업할 때 이전 예제에서 사용했다. 해당 스케줄러를 전환하려면 subscribeOn을 사용하면 된다.

Main.swift에는 백그라운드 큐를 사용하는 globalScheduler라는 미리 정의된 스케줄러가 있다. 이 스케줄러는 concurrent queue인 global dispatch queue를 사용하여 생성된다:

let globalScheduler = ConcurrentDispatchQueueScheduler(queue:
DispatchQueue.global())

따라서, 클래스의 이름에서 알 수 있듯이, 이 스케줄러가 계산할 모든 작업은 global dispatch queue에서 발송되고 처리됩니다.
이 스케줄러를 사용하기위해, 이전에 만든 과일 구독을 이 걸로 바꿔보자:

fruit
    .subscribeOn(globalScheduler)
    .dump()
    .dumpingSubscription()
    .disposed(by: bag)

이제 파일 끝에 다음 줄을 추가하자:

RunLoop.main.run(until: Date(timeIntervalSinceNow: 13))

이것은 사이에 끼어들어. 메인 스레드에서 글로벌 스케줄러와 observable을 dispose 하는 것과 같은 모든 작업이 완료되면 터미널이 종료되는 것을 방지한다. 이 경우, 터미널은 13초 동안 살아있을 거다. 예시에서 13초가 과도할 수 있지만 챕터를 진행하면서 앱을 완료하는 데 이 정도의 시간이 필요하다. 따라서 모든 관찰 가능 항목이 완료되면 앱을 자유롭게 중지가능하다.

이제 새 스케줄러가 제자리에 있으니, 빌드하고 실행하고 결과를 확인해보자

00s | [E] [dog] emitted on Main Thread
00s | [S] [dog] received on Main Thread
00s | [E] [apple] emitted on Anonymous Thread
00s | [S] [apple] received on Anonymous Thread
02s | [E] [pineapple] emitted on Anonymous Thread
02s | [S] [pineapple] received on Anonymous Thread
04s | [E] [strawberry] emitted on Anonymous Thread
04s | [S] [strawberry] received on Anonymous Thread

global queue은 이름이 없는 스레드를 사용하므로 이 경우 익명 스레드는 concurrent dispatch queue의 스레드 중 하나입니다.

이제, [E]emitter와 [S]subscriber 모두 같은 스레드에서 데이터를 처리하고 있다.

image

또한, observer가 연산자의 코드를 수행하는 곳을 변경하려면 어떻게 해야 할까? observeOn을 사용해보자.

observeOn 사용하기

Observing은 Rx의 세 가지 기본 개념 중 하나이다. 그것은 이벤트를 생성하는 엔티티와 그 이벤트에 대한 observer를 포함한다. 이 경우, subscribeOn에 반대하여, observeOn 연산자는 관찰이 일어나는 스케줄러를 변경할 수 있다.

따라서 이벤트가 Observable에 의해 푸시되면, 이 연산자는 구독자가 지정된 스케줄러에서 이벤트를 수신하도록 보장한다. 여기에는 관찰 후 추가한 모든 연산자도 포함된다!

현재 글로벌 스케줄러에서 메인 스레드로 전환하려면 구독하기 전에 observeOn을 호출해야 한다. 한 번 더, 과일 구독 코드를 교체해보자:

fruit
    .subscribeOn(globalScheduler)
    .dump()
    .observeOn(MainScheduler.instance)
    .dumpingSubscription()
    .disposed(by: bag)

빌드 및 실행하여 콘솔을 한 번 더 확인해보자:

00s | [E] [dog] emitted on Main Thread
00s | [S] [dog] received on Main Thread
00s | [E] [apple] emitted on Anonymous Thread
00s | [S] [apple] received on Main Thread
02s | [E] [pineapple] emitted on Anonymous Thread
02s | [S] [pineapple] received on Main Thread
04s | [E] [strawberry] emitted on Anonymous Thread
04s | [S] [strawberry] received on Main Thread

원하는 결과를 달성할 수 있다: 모든 이벤트는 이제 올바른 스레드에서 처리된다. 주요 observable은 백그라운드 스레드에서 이벤트를 처리하고 생성하는 것이며, subscriber는 메인 스레드에서 이벤트를 수신하고 있다.

image

우리는 매우 일반적으로 백그라운드 스케줄러를 사용하여 서버에서 데이터를 검색하고 수신된 데이터를 처리했으며, MainScheduler로만 전환하여 최종 이벤트를 처리하고 사용자 인터페이스에 데이터를 표시하는 패턴을 사용하기에 알아 둘 필요가 있다.

주의사항

스케줄러와 스레드를 전환하는 기능은 훌륭해 보이지만 몇 가지 함정이 있다. 그 이유를 알아보기 위해 새 스레드를 사용하여 일부 이벤트를 animal에 푸시해보자. 계산이 수행되는 스레드를 추적해야 하므로 OS 스레드를 사용하는 것이 좋은 솔루션이 될 거다.

let animalsThread = Thread() {
    sleep(3)
    animal.onNext("[cat]")
    sleep(3)
    animal.onNext("[tiger]")
    sleep(3)
    animal.onNext("[fox]")
    sleep(3)
    animal.onNext("[leopard]")
}

다음으로, 스레드의 이름을 지정하여 인식할 수 있도록 하고, 시작해보자:

===== Schedulers =====

00s | [E] [dog] emitted on Main Thread
00s | [S] [dog] received on Main Thread
00s | [E] [apple] emitted on Anonymous Thread
00s | [S] [apple] received on Main Thread
02s | [E] [pineapple] emitted on Anonymous Thread
02s | [S] [pineapple] received on Main Thread
03s | [E] [cat] emitted on Animals Thread
03s | [S] [cat] received on Animals Thread
04s | [E] [strawberry] emitted on Anonymous Thread
04s | [S] [strawberry] received on Main Thread
06s | [E] [tiger] emitted on Animals Thread
06s | [S] [tiger] received on Animals Thread
09s | [E] [fox] emitted on Animals Thread
09s | [S] [fox] received on Animals Thread
12s | [E] [leopard] emitted on Animals Thread
12s | [S] [leopard] received on Animals Thread

전용 스레드에서 동물을 생성했고, 전역 스레드에서 결과를 처리했다.
코드를 계속 추가한 다음 다른 것으로 교체하는 것이 단순 반복으로 보일 수 있지만, 여기서 목표는 다양한 스케줄러 간의 차이점을 비교하는자 한다.

animal subject에 대한 원래 구독을 다음 코드로 바꿔보자:

animal
    .dump()
    .observeOn(globalScheduler)
    .dumpingSubscription()
    .disposed(by: bag)

빌드 및 실행하여 콘솔을 확인해보자:

...
03s | [E] [cat] emitted on Animals Thread
03s | [S] [cat] received on Anonymous Thread
04s | [E] [strawberry] emitted on Anonymous Thread
04s | [S] [strawberry] received on Main Thread
06s | [E] [tiger] emitted on Animals Thread
06s | [S] [tiger] received on Anonymous Thread
09s | [E] [fox] emitted on Animals Thread
09s | [S] [fox] received on Anonymous Thread
12s | [E] [leopard] emitted on Animals Thread
12s | [S] [leopard] received on Anonymous Thread

이제 스레드를 교체하며 거의 13초 제한에 도달하는 걸 확인 할 수 있다.

global queue에서 animal을 생성하고 싶지만, 메인 스레드에서 받고 싶다면 어떨까?
animal을 생성하는 경우, observeOn을 우선하고, 구독하여 받는 경우에는 subscribeOn을 사용하면 될까?

이번에는 animal 구독을 다음으로 바꿔보자:

animal
    .subscribeOn(MainScheduler.instance)
    .dump()
    .observeOn(globalScheduler)
    .dumpingSubscription()
    .disposed(by: bag)

빌드 및 실행하여 콘솔을 확인해보자:

03s | [E] [cat] emitted on Animals Thread
03s | [S] [cat] received on Anonymous Thread
04s | [E] [strawberry] emitted on Anonymous Thread
04s | [S] [strawberry] received on Main Thread
06s | [E] [tiger] emitted on Animals Thread
06s | [S] [tiger] received on Anonymous Thread
09s | [E] [fox] emitted on Animals Thread
09s | [S] [fox] received on Anonymous Thread
12s | [E] [leopard] emitted on Animals Thread
12s | [S] [leopard] received on Anonymous Thread

계산이 원했던 방식으로 올바른 스케줄러에서 이루어지지 않는 걸 확인할 수 있다.
기본적으로 RxSwift를 비동기식 또는 다중 스레드로 생각하는 데서 오는 일반적이고 위험한 함정 중 하나다.

RxSwift와 일반적인 추상화는 기본적으로 프리 스레드다
데이터를 처리할 때 마법의 스레드 스위칭이 이루어지지 않으며, 달리 지정하지 않으면 계산은 항상 원래 스레드에서 발생한다.
모든 스레드 스위칭은 subscribeOn 및 observeOn 연산자를 사용하여 프로그래머의 명시적인 요청 후에 발생하게 된다.

RxSwift가 기본적으로 스레드 처리를 할 거라 생각하는 건 함정이다.
위에서 일어나는 예시는 Subject의 잘못된 사용이다.
원칙적인 계산 작업은 특정 스레드에서 발생하고 이러한 이벤트는 Thread() { ... }를 사용하여 해당 스레드에서 푸시된다.
Subject가 푸시되는 위치를 직접 제어할 수 없는 Subject의 특성으로 인해, RxSwift는 원칙적인 계산 작업의 스케줄러를 전환하고 다른 스레드로 이동하는 건 불가능하다,

근데 이게 왜 Animals Thread에서는 작동할까?
Observable.create(_:)를 사용하면 RxSwift가 스레드 블록 내에서 일어나는 일을 제어하여 스레드 처리를 더 미세하게 커스텀할 수 있기 때문!

이 예상치 못한 결과는 일반적으로 "Hot / Cold Observable" 문제로 알려져 있다.

위의 예시에선 Hot Observable을 다루고 있다. Observable은 구독 중에 부작용이 없지만, 이벤트가 생성되고 RxSwift가 제어할 수 없는 자체 컨텍스트가 있다(즉, 자체 스레드를 나타낸다).

대조적으로 Cold Observable은 관찰자가 구독하기 전에 어떤 요소도 생성하지 않는다. 즉, 구독 시 컨텍스트를 만들고 요소를 생성하기 시작할 때까지 자체 컨텍스트가 없다는 것을 잘 나타낸다.

Hot vs. cold

위의 섹션에서는 Hot / Cold Observable에 대해 다루었다. Hot / Cold Observable에 대한 주제는 상당히 독단적이며 많은 논쟁을 불러일으키므로 여기에서 간단히 살펴보자. 개념은 매우 간단한 질문으로 축소될 수 있다.

image

Side Effect의 몇 가지 예:

• 서버에 요청을 실행.
• 로컬 데이터베이스를 편집.
• 파일 시스템에 기록.
• 로켓을 발사.

Side Effect의 세계는 끝이 없으므로, Observable 인스턴스가 구독 시 Side Effect를 일으키는지 여부를 결정해야 한다. 확신할 수 없다면, 더 많은 분석을 수행하거나 소스 코드를 더 파헤쳐보자. 모든 구독에서 로켓을 발사하는 건 달성하고자 목표가 아닐 수도 있다...

이를 설명하는 또 다른 일반적인 방법은 Observable이 Side Effect를 공유하는지 여부를 확인하는 거다. 구독 시 Side Effect를 수행하는 경우, Side Effect가 공유되지 않는 걸 의미한다. 그렇지 않으면, Side Effect은 모든 구독자와 공유된다.

이것은 상당히 일반적인 규칙이며, Subject 및 관련된 모든 ObservableType 객체에 적용된다.

아시다시피, 우리는 지금까지 책에서 Hot / Cold Observable에 대해 많이 이야기하지 않았다. 반응형 프로그래밍의 일반적인 주제이지만, RxSwift에서는 위의 스레드 예제와 같은 특정 경우 또는 테스트를 실행할 때와 같이 더 큰 제어가 필요할 때만 개념을 접하기 때문이다.

이번 섹션을 참고점으로 유지하여, Hot / Cold Observable 측면에서 문제에 접근해야 할 경우, 책에 도움을 받아 개념을 새로 고치는 데 유용할 거다.

모범적인 활용과 내장 스케줄러

스케줄러는 사소한 주제이므로, 가장 일반적인 사용 사례에 대한 몇 가지 모범 사례가 제공된다. 이번 섹션에서는 Serial 및 Concurrent 스케줄러를 빠르게 소개하고, 데이터를 처리하는 방법을 배우고, 특정 컨텍스트에서 어떤 유형이 더 잘 작동하는지 확인할 수 있었다.

Serial vs. concurrent 스케줄러

스케줄러는 단순히 컨텍스트(디스패치 큐, 스레드, 사용자 지정 컨텍스트)일 수 있고, 시퀀스를 변환하는 모든 Operator가 암시적 보장을 유지해야 한다는 점을 고려해볼 때, 항상 올바른 스케줄러를 사용하고 있는지 확인해야한다.

  • Serial 스케줄러를 사용하는 경우 RxSwift는 작업을 순차적으로 수행합니다. Serial 디스패치 대기열의 경우 스케줄러는 아래에서 자체 최적화를 수행할 수 있다.
  • Concurrent 스케줄러에서 RxSwift는 코드를 동시에 실행하려고 시도하지만, observeOn 및 subscribeOn을 통해서 작업을 실행해야 하는 순서를 유지하고 구독 코드가 올바른 스케줄러에서 끝나는지 확인한다.

👉🏻 Serial vs. concurrent

Main 스케줄러

MainScheduler는 메인 스레드 위에서 동작한다. 해당 스케줄러는 UI의 변경 사항을 처리하고 다른 높은 우선 순위 작업을 수행하는 데 일반적으로 사용된다. iOS, tvOS 또는 macOS에서 애플리케이션을 개발할 때 일반적으로 MainScheduler를 사용하여 장기 실행 작업을 수행해서는 안 되므로 서버 요청이나 기타 무거운 작업에선 사용을 지양하자.

또한, UI를 업데이트하는 부작용을 수행하는 경우 해당 업데이트가 화면에 표시되도록 MainScheduler로 전환해야 한다.

MainScheduler는 또한 대부분의 RxCocoa Traits, 특히 Driver와 Signal을 사용할 때 모든 관찰에 사용된다. 이전 챕터에서 이야기 한 거 처럼 이러한 특성은 애플리케이션의 UI에 직접 데이터를 바인딩할 수 있는 기능을 제공하기 위해, 항상 MainScheduler에서 관찰이 수행되게 한다.

SerialDispatchQueue 스케줄러

SerialDispatchQueueScheduler는 SerialDispatchQueue에 대한 작업을 추상화한다. 이 스케줄러는 observeOn을 사용할 때 여러 최적화의 큰 장점이 있는데, 해당 스케줄러를 사용하여 Serial 방식으로 예약된 백그라운드 작업을 처리할 수 있다.

예를 들어 Firebase 또는 GraphQL 애플리케이션에서와 같이 서버의 단일 EndPoint와 통신하는 애플리케이션이 있는 경우, 수신측에 너무 많은 압력을 가하는 여러 동시 요청을 발송하는 것을 피하고 싶을 수 있다. 해당 스케줄러는 SerialDispatchQueue처럼 진행해야 하는 모든 작업에 대해 필요한 스케줄러다.

ConcurrentDispatchQueue 스케줄러

ConcurrentDispatchQueueScheduler는 DispatchQueue에 대한 추상 작업을 관리한다. 여기서 주요 차이점은 Serial Queue 대신 스케줄러가 Concurrent Queue을 사용한다는 것. 이런 종류의 스케줄러는 ObservOn을 사용할 때 최적화되지 않으므로, 어떤 종류의 스케줄러를 사용할지 결정할 때 이를 고려해야 한다.

ConcurrentDispatchQueue 스케줄러는 동시에 종료해야 하는 여러 장기 실행 작업에 적합한 옵션일 수 있다. 준비되었을 때 모든 결과가 함께 결합되도록 여러 개의 관측 가능 항목을 Blocking Operator와 결합하면 Serial 스케줄러가 최상의 성능을 발휘하지 못할 수 있다. 대신, Concurrent 스케줄러는 여러 개의 동시 작업을 수행하고 결과 수집을 최적화할 수 있다.

OperationQueue 스케줄러

OperationQueue 스케줄러는 ConcurrentDispatchQueue 스케줄러와 유사하지만 DispatchQueue를 통해 작업을 추상화하는 대신 OperationQueue를 통해 작업을 수행한다. 실행 중인 동시 작업에 대한 더 많은 제어가 필요한 경우가 있을 때, 동시ConcurrentDispatchQueue에서는 이 작업을 수행할 수 없다.
최대 동시 작업 수를 세부적으로 조정해야 하는 경우 사용하는 스케줄러로 응용 프로그램의 필요에 맞게 동시 작업 수를 제한하도록 maxConcurrentOperationCount를 설정할 수 있다.

Test 스케줄러

Test 스케줄러는 테스트에만 사용되도록 되어 있는 특별한 종류의 스케줄러이다. 생산 코드에 사용해서는 안 되며, 해당 특수 스케줄러는 RxTest 라이브러리의 일부인 Operator 테스트를 단순화한다. 테스트에 대한 전용 챕터에서 이 스케줄러를 면밀히 사용하는 방법을 살펴보게 될 것이다.
👉🏻 예제 코드

// Observable+DelaySubscriptionTests.swift
func testDelaySubscription_TimeSpan_Simple() {
    // 초기화 된 Test 스케줄러
    let scheduler = TestScheduler(initialClock: 0)
    
    // 비교할 Test Observable을 생성하고
    let xs = scheduler.createColdObservable([
        .next(50, 42),
        .next(60, 43),
        .completed(70)
    ])

    // scheduler를 실행    
    let res = scheduler.start {
        xs.delaySubscription(30, scheduler: scheduler)
    }
    
   // 해당 스트림과 비교 - 스트림 발생 시간 설정
    XCTAssertEqual(res.events, [
        .next(280, 42),
        .next(290, 43),
        .completed(300)
    ])
        
   // 해당 스트림과 비교 - 스트림 발생 시간 설정
    XCTAssertEqual(xs.subscriptions, [
        Subscription(230, 300)
    ])
}

Where to go from here?

스케줄러는 RxSwift에서 중요하지 않은 주제; 스케줄러는 RxSwift에서 모든 작업을 계산하고 수행한다. 스케줄러의 가장 중요한 규칙은 무엇이든 될 수 있다는 것! 이 점을 명심하면 관찰할 수 있는 작업이나 스케줄러 사용 및 변경 작업을 할 때 잘 활용할 수 있다.

앞에서 읽은 것처럼 스케줄러는 Dispatch Queue, OperationQueue, 스레드 위에 배치되거나 현재 스레드에서 즉시 작업을 수행할 수 있다. 이에 대한 엄격한 규칙은 없지만, 현재 작업에 어떤 스케줄러를 사용하고 있는지 확인하면서 진행하는 건 필요하다. 때때로 잘못된 스케줄러를 사용하면 성능에 부정적인 영향을 미칠 수 있지만, 잘 선택한 스케줄러는 큰 성과를 거둘 수 있기 떄문에 늘 고려해보자.

예제를 구성하는데 시간을 쓰고, 일부 스케줄러를 테스트하여 최종 결과에 어떤 영향을 미치는지 확인해보자. 스케줄러를 이해하면 RxSwift의 구성이 더 쉬워지고 subscribeOn 및 serviseOn을 사용할 때 자신감이 붙는다.

@simoniful simoniful added the Rx label May 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant