[RxJava] FlatMap,SwitchMap,ConcatMap differences

Jay·2021년 1월 26일
0

RxJava

목록 보기
3/7
post-thumbnail

Intro

Observable에서 발행된 아이템을 변환하는 부분은 Rx에서 중요한 부분이다.
그에 해당하는 흔한 3가지 연산자의 장단점을 알아보려 한다.

FlatMap

  • 변환 연산자

먼저, String 리스트를 만들고 from을 통해서 목록의 각 객체를 Observalbe로 변환하자.
그리고서 각 항목의 문자열 끝에 "x"를 추가하여 관찰 가능한 항목으로 flapMapped이 될 것이다.
그리고서 10이하의 난수를 생성해서 랜덤하게 딜레이를 준다.
마지막으로 모든 항목이 방출 될 시간이 충분하였는지 확인하기 위해 1분 딜레이를 해주게 된다.


@Test
public void flatMap() throws Exception {
    final List<String> items = Lists.newArrayList("a", "b", "c", "d", "e", "f");

    final TestScheduler scheduler = new TestScheduler();

    Observable.from(items)
            .flatMap( s -> {
                final int delay = new Random().nextInt(10);
                return Observable.just(s + "x")
                        .delay(delay, TimeUnit.SECONDS, scheduler);
            })
            .toList()
            .doOnNext(System.out::println)
            .subscribe();

    scheduler.advanceTimeBy(1, TimeUnit.MINUTES);
}

결과는 아래와 같이 나온다.

[cx, ex, fx, bx, dx, ax]

FlatMap은 이러한 Observable의 방출을 결합하여 상호 배치 할 수 있다.

그러나 각 항목마다 랜덤한 수로 딜레이를 주었기에 순서가 보장되지 못하였다.
각 항목에 대해 새로운 observable을 생성하고 각각의 observable은 독립적으로 존재한다. 위의 각 항목 중 일부는 빠르게 방출되고 일부는 느리게 방출될 것이다. (랜덤하게 딜레이를 주었기에)
딜레이를 주지 않더라도 비슷한 효과를 볼 수 있지만 더 직관적으로 보기 위해 딜레이를 랜덤하게 주었다.


SwitchMap

  • 조합 연산자

flatMap과 동일 조건으로 같은 테스트를 할 것이다. 연산자만 switchMap으로 바뀌게 된다.

@Test
public void switchMap() throws Exception {
    final List<String> items = Lists.newArrayList("a", "b", "c", "d", "e", "f");

    final TestScheduler scheduler = new TestScheduler();

    Observable.from(items)
            .switchMap( s -> {
                final int delay = new Random().nextInt(10);
                return Observable.just(s + "x")
                        .delay(delay, TimeUnit.SECONDS, scheduler);
            })
            .toList()
            .doOnNext(System.out::println)
            .subscribe();

    scheduler.advanceTimeBy(1, TimeUnit.MINUTES);
}

결과는 아래와 같다.

[fx]

공식 문서에 이유에 대해 잘 설명되어있다.

📢 Observable 소스로부터 새로운 아이템이 방출될때마다, 이전에 방출된 아이템에 대한 Observable 구독을 해지하고 미러링을 중단한다.
그리고 오직 현재의 아이템만 미러링을 한다.

whenever a new item is emitted by the source Observable, it will unsubscribe to and stop mirroring the Observable that was generated from the previously-emitted item, and begin only mirroring the current one.

오직 단일 아이템만 구독과 미러링을 하는 SwitchMap


ConcatMap

  • 계산 및 집합 연산자

연산자만 concatMap으로 바꿔서 동일 테스트를 진행해야지.

@Test
public void switchMap() throws Exception {
    final List<String> items = Lists.newArrayList("a", "b", "c", "d", "e", "f");

    final TestScheduler scheduler = new TestScheduler();

    Observable.from(items)
            .concatMap( s -> {
                final int delay = new Random().nextInt(10);
                return Observable.just(s + "x")
                        .delay(delay, TimeUnit.SECONDS, scheduler);
            })
            .toList()
            .doOnNext(System.out::println)
            .subscribe();

    scheduler.advanceTimeBy(1, TimeUnit.MINUTES);
}

결과는 아래와 같다.

[ax, bx, cx, dx, ex, fx]

ConcatMap은 flatMap과 매우 유사하다. 그러나 아이템의 순서를 보장해준다.🗯

그러나 concatMap은 한 가지 큰 결함이 있다..
다음 작업이 처리 될 때까지 각 Observable이 모든 작업을 완료 할 때까지 기다린다.🙀

@Test
public void flatMapAndConcatMapCompare() throws Exception {
    final List<String> items = Lists.newArrayList("a", "b", "c", "d", "e", "f");

    final TestScheduler scheduler1 = new TestScheduler();
    final TestScheduler scheduler2 = new TestScheduler();

    Observable.from(items)
            .flatMap(s -> Observable.just(s + "x")
                    .delay(5, TimeUnit.SECONDS, scheduler1)
                    .doOnNext(str -> System.out.print(scheduler1.now() + " ")))
            .toList()
            .doOnNext(strings -> System.out.println("\nEND:" + scheduler1.now()))
            .subscribe();

    scheduler1.advanceTimeBy(1, TimeUnit.MINUTES);

    Observable.from(items)
            .concatMap(s -> Observable.just(s + "x")
                    .delay(5, TimeUnit.SECONDS, scheduler2)
                    .doOnNext(str -> System.out.print(scheduler2.now() + " ")))
            .toList()
            .doOnNext(strings -> System.out.println("\nEND:" + scheduler2.now()))
            .subscribe();

    scheduler2.advanceTimeBy(1, TimeUnit.MINUTES);

}

두 가지 연산자(flatMap,concatMap)의 차이를 완벽히 비교할 수 있도록 두개의 스케쥴러를 만들었다.
결과는 아래와 같다.

5000 5000 5000 5000 5000 5000
END:5000
5000 10000 15000 20000 25000 30000
END:30000

간단한 테스트 만으로 concatMap은 사용하기 편하지만 비동기를 망칠 수 있고 전체 프로세스를 길게 만들 수 있기에 주의해서 사용해야 한다는 점을 알았다.


Best Practice

  1. 새로운 데이터를 다룬다고 가정하자. Application 어딘가에서 객체 리스트를 주기적으로 내보내는 Observable이 있다. 사용자가 상호작용할 때마다 새로 고쳐지는 타임라인의 게시물 목록일 수 있다.
    이러한 경우엔 switchMap 연산자가 적당하겠다. 우리가 새로운 결과를 받게된다면 이전의 결과는 관심없으니까?
    이전의 데이터를 구독 해지하고 새로운 데이터에 집중하는 것이 안전하다.
    이전의 데이터를 스킵하는 프로세스 시간을 줄여주는 좋지 아니한가?

  2. 리스트에서 각각의 아이템에 대한 특정한 데이터를 가져온다고 하자.
    (ex. 연락처 목록에서 각 사용자에 대한 이름?)
    이러한 경우, concatMap 연산자가 적당하겠다.
    flatMap을 여기서 사용한다면, 연락처 목록내의 순서가 뒤죽박죽일 수 있다.
    순서를 보장할 수 있는 메커니즘을 구축했다면 당연히 flatMap을 사용하는 것도 좋은 방법이다.
    switchMap이 오면 안된다. 왜냐면 위에서 말한대로 모든 연락처의 정보를 얻을 수 없게 되기 때문에.

  3. 정렬된 리스트에서 각 아이템에 무언가를 한다고 치자.
    FlatMap과 SwitchMap은 사용하면 안된다. 오직 ConcatMap만 리스트를 같게 유지해주기에 가능하다. (이쯤되면 감이 오지 않을까..?)
    concatMap의 동기 호출로 인해 처리 시간이 증가 될 수 있단 점 정도는 고려하자.

  4. 리스트에서 각 아이템에 정보를 보낸다고 생각해보자.
    (여기서 정보를 보내는건... 각 포스트에 메시지를 보내는 것?)
    모든 요청을 보장하기 위해서는 switchMap을 사용해선 안된다. (got it?)
    flatMap과 concatMap 둘 다 사용할 수 있겠다. 근데 순서를 생각하지 않는다면 flatMap이 훨씬 좋겠다. 그리고 빠르게 결과를 받고 한번에 요청할 수 있으니까!

  5. 쿼리를 통해 아이템을 검색하는 경우.
    사용자가 x를 입력하고 y를 입력하는 상황이라고 해보자.
    전체 쿼리는 xy가 되겠지? 그렇기에 x에 대한 단일 구독은 사실상 필요가 없다. switchMap이 되겠다. 이해가 된다. 앞의 구독을 해지하고 늘 최신만 옵저빙하기에 x 다음 xy가 된 그 다음을 옵저빙하면 된다. Got it?👍


코드 최적화를 고려하지 않는다면 concatMap을 쓰면 다 잘 될 것이다.
그렇지만.. 주의는 하자!

Summary 📝

flatMap : 아이템의 순서를 보장하지 않고, 비동기적으로 동작한다.(빠르다)
switchMap : 이전의 observable은 구독하지 않고 늘 최신만 구독한다.
concatMap : 순서를 보장한다. 근데 동기적으로 동작한다.(느리다)

profile
developer

0개의 댓글