Android - RxJava를 왜 사용해야할까요? RxJava와 리액티브 프로그래밍 정리 (Kotlin)

업데이트:

리액티브 프로그래밍 - RxJava

image

이전의 모바일 애플리케이션은 클라이언트가 서버에 요청을 하고, 서버가 일을 다 처리한 후 클라이언트가 값을 받아 뷰를 렌더링하는 동기식 구조가 대부분이었습니다.

하지만 기술이 발전하고 요구사항이 늘어남에 따라, 애플리케이션의 규모가 커지고 복잡해졌습니다. 그래서 이제는 기존의 동기식 방식으로는 한계가 있었습니다.

따라서 현재 대부분의 애플리케이션은 복잡한 비동기식 요청으로 프로그램이 개발되어 동작하게 되었습니다.

리액티브 프로그래밍을 이용하면 이러한 비동기식 요청을 효과적으로 처리할 수 있습니다.

이 때, 등장한 것이 RxJava 입니다. RxJava는 이러한 리액티브 프로그래밍 방식을 구현한 확장 라이브러리라고 할 수 있습니다.


지금껏 AsyncTask나 ThreadPool을 이용해서 비동기 프로그래밍을 잘 구현해왔는데요? 꼭 리액티브 프로그래밍을 해야하나요?

네. 간단한 로직의 경우 기존에 존재하는 비동기 구현방식으로 개발해도 별 문제가 없습니다.
하지만 복잡한 프로그램의 경우 얘기가 달라집니다.

기존의 방식에도 한계가 있었습니다. 비동기를 제어한다는 것이 생각보다 많이 까다롭습니다.
로직이 많아지고 복잡해지면 프로그램에 버그도 많이 생길 수 있고 유지보수하기도 까다로워집니다.

바로 이러한 문제점들을 어느정도 해소해줄 수 있는 것이 바로 리액티브 프로그래밍 인거죠.

그럼 리액티브 프로그래밍이 어떻길래 이토록 각광받고 있는걸까요?


리액티브 프로그래밍은 무엇일까요?

리액티브 프로그래밍은 위키피디아의 정의에 따르면, 변화의 전파와 데이터 흐름과 관련된 선언적 프로그래밍 패러다임입니다.

  • 변화의 전파와 데이터 흐름: 데이터가 변경될 때 마다 이벤트를 발생시켜서 데이터를 계속적으로 전달

  • 선언형 프로그래밍: 명령형 프로그래밍과 달리 구체적인 동작을 명시하기보다 단순히 목표만을 선언


명령형 프로그래밍

그럼 좀전에 설명했던 선언형 프로그램밍이라는게 무슨 말일까요?

다음은 기존에 사용하던 명령형 프로그래밍의 예시입니다.

코드는 6보다 큰 홀수들의 합을 구하는 프로그램입니다.
구체적인 동작 알고리즘이 함수 내에 표현이 되어 동작하는 것을 알 수 있습니다.

fun main(args: Array<String>) {
    var numbers = arrayListOf<Int>(1, 5, 10, 7, 21)
    var sum = 0

    for (n in numbers) {
        if (n > 6 && (n % 2 != 0)) {
            sum += n
        }
    }
    println(sum)
}


선언형 프로그래밍

다음 코드를 봅시다.
stream()으로 선언형 프로그래밍 방식을 간단하게 구현할 수 있습니다.
filter 함수에서 데이터의 조건을 주어서 필터링하겠다고 선언합니다. 구체적인 알고리즘은 표현되지 않습니다.
그리고 sum() 함수를 통해 합계를 구합니다.

이렇듯 선언형 프로그래밍 방식은 구체적인 알고리즘을 명시하지 않고, 해당 함수를 사용하여 그저 선언만 하게됩니다.

fun main(args: Array<String>) {
    var numbers = arrayListOf<Int>(1, 5, 10, 7, 21)
    var sum = numbers.stream()
        .filter{number -> number > 6 && (number % 2 != 0)}
        .mapToInt{number -> number}
        .sum()
    println(sum)
}


리액티브의 동작

기존 명령형 프로그래밍 방식은 pull 방식입니다. 즉, 변경된 데이터가 있는지 요청을 보내 절의하고 변경된 데이터를 가져오는 방식입니다.
기존 클라이언트 요청, 서버 응답 방식의 애플리케이션과 Java 같은 절차형 프로그래밍 언어가 대표적인 pull 방식입니다.

반면 리액티브 프로그래밍 방식은 push 방식으로, 데이터의 변화가 발생했을 떄 변경이 발생한 곳에서 데이터를 보내주는 방식입니다.
소켓 프로그래밍, 스마프톤에서 이벤트가 발생했을 때, 오는 push 메시지가 대표적인 push 방식의 예로 말할 수 있습니다.


리액티브 프로그래밍의 요소

리액티브 프로그래밍을 위해 알아야 할 것들이 몇 가지 존재합니다.

  • Observable: 데이터 소스
    • 지속적으로 변경 가능한 데이터의 집합으로 앞으로 변경되는 데이터를 관찰합니다.
  • 리액티브 연산자(Operators): 데이터 소스를 처리하는 함수
    • 데이터를 처리하는 함수로서, 전달 받은 데이터를 가공하여 최종적인 데이터를 만들어냅니다.
  • 스케쥴러(Scheduler): 쓰레드 관리자
    • 리액티브 프로그래밍은 비동기 프로그래밍을 위한 기법이기 때문에, 그 쓰레드를 관리해줍니다.
  • Subscriber: Observable이 발행하는 데이터를 구독하는 구독자
    • 데이터에 대한 구독을 해야 Observable이 발행하는 데이터를 전달받을 수 있습니다.
  • 함수형 프로그래밍: RxJava에서 제공하는 연산자 함수를 사용
    • 쓰레드 기반 프로그래밍은 예상하지 못한 버그 발생과 그것을 제어하는 것이 어렵습니다.
    • 리액티브 프로그래밍에서는 이러한 문제를 해결하기위해 순수 함수를 지향하는 함수형 프로그래밍을 도입하여 사용합니다. 이러한 기법으로 작성된 함수가 리액티브 연산자 함수입니다.


RxJava 사용 예시

기본 동작

다음은 RxJava를 사용한 간단한 예시입니다.
Observable.just 함수를 통해 데이터를 발행합니다.
filter 함수를 통해 300 이상의 데이터만 필터링 합니다.
subscribe 함수를 통해 발행된 데이터 -> 필터링된 데이터를 받아서 화면에 출력합니다.

따라서 전체적으로 RxJava가 동작하는 흐름은 다음과 같습니다.

데이터 발행 -> 데이터 가공 -> 데이터 구독 및 처리

fun main(args: Array<String>) {
    Observable.just(100,200,300,400,500)
        .filter{n -> n > 300}
        .subscribe(){n -> println("${Thread.currentThread().getName()} : result: ${n}")}
}


이 상태에서 프로그램을 실행하면 다음과 같이 출력됩니다.
실행 결과 출력이 메인쓰레드에서 400과 500이 출력된 것을 확인할 수 있습니다.

image


doOnNext

그럼 여기서 doOnNext라는 함수를 한번 추가 해보겠습니다.

fun main(args: Array<String>) {
    Observable.just(100,200,300,400,500)
        .doOnNext{ data -> println("${Thread.currentThread().getName()} : doOnNext: ${data}")}
        .filter{n -> n > 300}
        .subscribe(){n -> println("${Thread.currentThread().getName()} : result: ${n}")}
}


실행도 바로 시켜보겠습니다.

image

네. doOnNext 함수는 다음과 같이 데이터가 처음 발행될 때, 호출됩니다.
그래서 결과를 보시면 100부터 500까지 메인 쓰레드에서 출력된 것을 확인할 수 있습니다.


근데 잘 생각해보면 처음에 RxJava는 비동기 프로그래밍을 위한 라이브러리 라고 했습니다. 근데 왜 메인쓰레드에서 동작할까요?

다른 쓰레드에서 동작하게 하려면 subscribeOn 함수에 Schedulers를 등록해야 합니다.


비동기 동작

다른 쓰레드를 사용하기 위해 이번에는 subscribeOn 함수를 넣어줍니다.

fun main(args: Array<String>) {
    Observable.just(100,200,300,400,500)
        .doOnNext{ data -> println("${Thread.currentThread().getName()} : doOnNext: ${data}")}
        .subscribeOn(Schedulers.io())
        .filter{n -> n > 300}
        .subscribe(){n -> println("${Thread.currentThread().getName()} : result: ${n}")}
}


결과는 다른 쓰레드에서 동작할까요?

image


결과로 아무것도 출력되지 않았습니다. 왜일까요?
왜냐하면 subscribeOn 함수로 다른 쓰레드를 지정함으로 인해서 모든 과정이 비동기로 동작하기 때문입니다.

그렇기 때문에 해당 쓰레드가 시작하여 데이터를 발행하여 처리하기도 전에 메인 쓰레드로 동작하고 있는 메인함수가 종료되어 프로그램이 종료된 것이지요.

그렇다면 테스트를 위해서 잠시 메인 쓰레드를 쉬어보겠습니다.
다음 명령문을 추가합니다.

Thread.sleep(500);
fun main(args: Array<String>) {
    Observable.just(100,200,300,400,500)
        .doOnNext{ data -> println("${Thread.currentThread().getName()} : doOnNext: ${data}")}
        .subscribeOn(Schedulers.io())
        .filter{n -> n > 300}
        .subscribe(){n -> println("${Thread.currentThread().getName()} : result: ${n}")}
    
    Thread.sleep(500);
}


실행해봅니다.

image


이번에는 스케쥴러라는 이름을 가진 쓰레드가 동작하여 이 쓰레드로 인해 데이터가 처리되어 출력된 것을 확인할 수 있습니다.

이렇게 subscribeOn 함수를 이용하면 메인 쓰레드가 아닌 다른 쓰레드에서 동작하게 할 수 있습니다.


그럼 이번에는 observeOn이라는 함수를 추가해보도록 하겠습니다.

fun main(args: Array<String>) {
    Observable.just(100,200,300,400,500)
        .doOnNext{ data -> println("${Thread.currentThread().getName()} : doOnNext: ${data}")}
        .subscribeOn(Schedulers.io())
        .observeOn(Schedulers.computation())
        .filter{n -> n > 300}
        .subscribe(){n -> println("${Thread.currentThread().getName()} : result: ${n}")}

    Thread.sleep(500);
}


실행 시켜 보겠습니다.

image


결과를 보시면 데이터가 처음 발행될 때는 RxIoScheduler-2 쓰레드에서 처리되고 있습니다.

그리고 발행된 데이터를 가공하고 구독하여 처리된 데이터는 RxComputationThreadPool-1 쓰레드가 처리하고 있습니다.


결론적으로 subscribeOn 함수는 데이터의 발행과 흐름을 결정짓는 쓰레드를 지정하여 동작합니다.

그리고 observOn 함수는 발행된 데이터를 가공하고 구독하여 처리하는 쓰레드를 지정하여 동작합니다.

즉, 이 둘은 바동기 프로그래밍에서 스케줄러를 결정짓는 함수들입니다.


마블 다이어그램

마블 다이어그램은 리액티브 프로그래밍에서 사용되는 핵심인 연산자를 더욱 쉽게 이해할 수 있도록 해줍니다.

즉, 리액티브 프로그래밍을 통해 발생하는 비동기적인 데이터의 흐름을 시간의 흐름에 따라 시각적으로 표시한 다이어그램 입니다.

아래의 그림은 한 가지 예시입니다.

image


위에 있는 도형들은 발행된 데이터이며 왼쪽부터 순차적으로 진행됩니다.

아래의 도형들은 처리하여 가공된 데이터를 의미합니다. 마찬가지로 순차적으로 나타내어집니다.

그리고 가운데에 있는 filter는 연산을 의미하겠죠? 이렇게 데이터의 발행과 가공, 처리 그리고 종료까지를 시각화하여 나타나 있기 때문에 연산자를 이해하기 훨씬 간단합니다.

마블 다이어그램은 리액티브 프로그래밍을 다룰 경우 반드시 접할 수 밖에 없기 때문에 잘 숙지하시길 바랍니다.


결론

정리하여 리액티브 프로그래밍의 RxJava는 전체적으로 다음과 같이 동작합니다.

데이터의 발행 -> 데이터의 가공 -> 데이터 구독 및 처리

이러한 전체적인 흐름을 숙지하고 RxJava를 학습하여 프로그래밍 하시면 됩니다.

그리고 이러한 리액티브 프로그래밍 핵심이 되는 연산자를 쉽게 이해할 수 있는 마블 다이어그램도 잘 이해하시면 도움이 많이 될 것 입니다.

마블 다이어그램에 대한 상세한 설명은 다음 포스팅으로 진행하도록 하겠습니다.


댓글남기기