RxJava 개념 정리 및 학습

wonseok·2022년 8월 15일
1
post-thumbnail

📌 RxJava vs RxKotlin

안드로이드 앱을 개발하고 있는 여러분에게는 이런 의문점이 생길 것이다.

"나는 분명히 코틀린을 사용하고 있는데 왜 RxJava에 대한 글을 읽고 있지?"

좋은 질문이다!
RxJava는 코틀린이 주류 프로그래밍 언어로 인정받기 전인, 2013년도부터 존재해왔으며,
자바와 100% 호환 가능한 언어인 코틀린을 위한 RxJava를 처음부터 다시 만드는 것도
말이 안되는 일이였다.
그냥 기존에 있었던 RxJava 라이브러리를 활용하면 되는데 말이다!

그렇지만, RxJava가 코틀린을 위해 완전히 재작성 될 필요가 없다는 이유로 코틀린 언어의 장점을 수혜받지 못한다는 것은 또 말이 안되었다...

그래서 RxKotlin이 등장했다!

RxKotlin은 쉽게 말해 RxJava에 수많은 유틸과 확장 함수들을 추가함으로써 확장시킨 라이브러리라고 할 수 있다!

그럼 RxJava는 뭔데?

영어 본문을 해석하지 않고 일단 그대로 가져오겠다.

RxJava is a library for composing asynchronous and event-based code by using observable sequences and functional style operators, allowing for parameterized execution via schedulers.

🤦 읽기만해도 복잡해보이지 않는가?

쉽게 풀어쓰자면, RxJava는 비동기 프로그램을 단순화하도록 도와주며, 
이는 새로운 데이터에 반응하도록, 그리고 그것을 연속적, 고립된 방법으로 구현한다. 
다시 말해, RxJava는 앱에서 일련의 비동기 이벤트들을 관찰하도록 하고, 
그에 맞추어서 각각의 이벤트들에 반응한다.
참고로, 앱 상에서 유저에 의해 터치되는 탭 이벤트들과 그 결과를 비동기 네트워크 요청에 
사용하는 것을 에로 들 수 있다.

📌 비동기 프로그래밍 개요

다음은 일반적인 안드로이드 앱에서 일어날 수 있는 기능들이다.

  • 버튼 탭에 반응하기
  • 스크린 상에서 뷰를 움직이도록 애니메이션 처리하기
  • 인터넷을 통해 용량이 큰 사진 파일을 다운로드하기
  • 디스크에 데이터 저장하기
  • 음악 재생하기

얼핏보기에 이 모든 기능들은 동시에 일어날 수도 있을 것 같다.

키보드가 스크린상에서 펼쳐지는 애니메이션이 발생할 때마다, 
그 애니메이션이 다 끝나기 전까지 앱에서 효과음을 계속 내고 있지 않는가?

안드로이드 운영체제는 당신에게 다양한 작업들을 각기 다른 쓰레드에서 수행하도록 도와주고, 기기의 CPU가 갖고 있는 각기 다른 코어에서 수행할 수 있도록 해준다.


안드로이드 비동기 API

구글은 당신으로 하여금 비동기 코드를 작성하는데에 도움을 주고자 몇가지 다양한 API를 제공한다.

  • AsyncTask
  • IntentService
  • Thread
  • Future

물론 이것이 끝이 아니다.

Handler, JobScheduler, WorkManager, HandlerThread, Kotlin Coroutines 등이 있다.

Coroutines와 RxJava

안드로이드 개발 생태계에서 코틀린 코루틴은 점점 큰 비중을 차지해가고 있으며, 이런 상황 속에서 이 글을 보고 있는 당신은 아마도 아직도 RxJava를 공부해야 하는지 의문점이 들 수도 있다.

그러나 실제로, RxJava와 코루틴은 다른 추상화 레벨에서 동작한다.

코루틴은 쓰레딩에 경량적인 접근 방식을 제공하고 비동기 코드를 동기적인 방식으로 작성할 수 있도록 도와준다.
반면, Rx는 위에서 언급했던 예시처럼 주로 이벤트 기반 아키텍쳐(event-driven architecture)에서 사용되며, 반응형 앱을 만들 수 있도록 도와준다.

결국, 두 가지 방식 (코루틴, RxJava) 모두 메인 쓰레드에서 벗어나 비동기 작업을 할 수 있도록 도와주는 역할을 하지만, 그들은 실제로 다른 상황에서 유용하게 적용될 수 있는 다른 도구라고 볼 수 있다!


비동기 프로그래밍의 어려움

대부분의 일반적인 클래스는 비동기적으로 무언가를 수행하며, 모든 UI 컴포넌트들을 본질적으로 비동기적이므로, 앱 코드 전체가 어떤 순서로 실행될지 정확하게 추측하는 것인 불가능에 가깝다.

결국, 당신이 만든 앱의 코드는 여러가지 외부적인 요소에 의해 크게 달라질 수 있다는 점이다!

예를 들어, 당신이 AsyncTask를 사용하여 UI를 업데이트하고, IntentService를 통해 무언가를 데이터베이스에 저장하고, WorkManager를 사용하여 앱을 서버와 동기화할 수 있다.
그런데, 이러한 모든 비동기 API들에 대한 통일적인 언어가 없기 때문에, 코드를 읽고 결과를 추론해내기가 매우 어려워진다.

코드를 통해 동기와, 비동기 코드에 대한 예시를 더 살펴보자.

동기 코드

var list = listOf(1, 2, 3)
for (number in list) {
  println(number)
  list = listOf(4, 5, 6)
}
print(list)

비동기 코드

var list = listOf(1, 2, 3)
var currentIndex = 0
button.setOnClickListener {
  println(list[currentIndex])
  if (currentIndex != list.lastIndex) {
    currentIndex++
  }
}

비동기 코드를 동기 코드와 비슷한 관점에서 바라보자.
유저가 버튼을 클릭할 때마다 리스트의 모든 원소들을 출력하는가?

그렇지 않다!

예를 들어 위의 비동기 코드는 모든 원소들을 출력하기 전에 어떤 비동기 코드가 리스트의 마지막 원소를 삭제할 수 있고, 새로운 원소를 리스트의 맨 앞에 추가할 수가 있다.

당신은 클릭 리스너만이 currentIndex값을 변화시킬 것이라고 예상하지만, 실제로 다른 코드가 currentIndex값을 수정할 수가 있다.

이러한 문제에서 RxJava는 그 진가를 발휘한다! 💪

비동기 프로그래밍 용어 사전

  1. State, Mutable State
    State는 정확히 정의 내리기는 어렵다. state를 이해하기 위해서는 다음의 예시를 먼저 이해해야 한다.
    예) 만약 당신이 노트북을 사용한다고 생각해보자. 며칠 동안 혹은 몇 주 동안은 사용하는데에 전혀 문제가 없다가, 갑자기 당신의 노트북이 맛탱이가 갔다고 쳐보자. 분명 하드웨어와 소프트웨어는 똑같은데, 변한 것은 state 뿐이다. 노트북을 재부팅하자마자, 똑같은 하드웨어와 소프트웨어의 조합인 당신의 노트북은 다시 잘 작동한다.
    메모리 데이터, 디스크에 저장된 데이터, 유저 입력에 따른 모든 부산물들, 클라우드 서비스로 데이터를 최신화한 후에 생긴 모든 흔적 파일들이 당신 노트북의 state라고 할 수 있다.

  2. Imperative Programming (명령형 프로그래밍)
    명령형 프로그래밍은 프로그램의 state를 변경하는 명령들을 사용하는 프로그래밍 패러다임이라고 보면 된다.

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setupUI()
  bindClickListeners()
  createAdapter()
  listenForChanges()
}

다음의 코드들은 각각의 메서드들이 무엇을 하는지 말해주지 않는다.
더 혼란스러운 것은 저 메서드들이 올바른 순서로 호출되는가이다.
아마 어떤 이는 실수로 저 메서드들의 호출 순서를 바꾸고, 커밋을 할 수 있을 것이다.

한 프로그래머의 실수로 발생한 이러한 스와핑 때문에 앱이 다르게 동작할 수도 있다.
  1. Side effects
    Side effects는 현재의 스코프 바깥에서 일어나는 어떠한 state의 변화를 말한다.
    다시말해 어떤 함수가 함수 안에 정의된 지역변수 이외에 어떤 state를 변경한다면, 그 함수는 side effect를 발생한다고 볼 수 있다.

  2. Declarative code (선언형 코드)
    명령형 프로그래밍에서 프로그래머는 의지에 따라 state를 변경할 수 있다.
    함수형 프로그래밍은 이와는 정반대되는 개념이다.
    함수형 코드에서는 어떠한 side effects도 발생시키지 않는다.

    여기서 RxJava는 이러한 명령형 코드와 함수형 코드의 최고 장점만 골라서 합친다!

side effects 를 일으키지 않을 뿐더러, 함수형 코드는 선언형(declarative)이다.

다시말해, 코드는 how that encompasses the imperative way of programming 할 때가 아니라 what you want to do 에 초점을 둘 때, 선언적(declarative)이다고 볼 수 있다!

  1. Reactive Systems
  • Responsive
  • Resilient
  • Elastic
  • Message driven

요약하면, reactive systems는 유저와 다른 이벤트들에 유연하고 일관성 있는 방식으로 반응한다.


📌 RxJava 들어가기

👉 처음에 이 그림을 봤을 때 한줌의 미역 줄기인 줄 알았는데, 
알고보니 이것은 전기뱀장어를 형상화한 것이라고 한다. 
(Rx 프로젝트는 원래 Volta라고 불렸다.)

이제부터 본격적으로 observables, operators, schedulers에 대해서 자세히 설명하겠다.

Observables

Observable<T> 클래스는 Rx 코드의 기반을 제공해주는데, 
불변성의 T 데이터를 "운반"할 수 있는 일련의 이벤트들을 비동기적으로 생산할 수 있는 기능이 그것이다.
쉽게 말하면, Observable은 어떤 클래스들로 하여금, 다른 클래스에서 emitted된 값들을 
구독할 수 있도록 하는 것이다.

Observable<T> 클래스는 모든 종류의 이벤트들에 반응하여 실시간으로 앱의 UI를 변경할 수 있도록, 혹은 새롭게 들어오는 데이터를 가공할 수 있도록, 한 명 이상의 관찰자를 허용한다.

ObservableSource<T> 인터페이스(Observable<T> 클래스를 구현한)는 굉장히 간단하다.
Observable은 다음의 세가지 타입 중 아무거나 emit할 수 있다. (그러면 관찰자가 전달받음)

  • a next event
    가장 나중 혹은 최신의 데이터 값을 들고 있는 이벤트를 말한다.
    이것이 바로 관찰자들이 데이터를 "전달받는" 방법이다.
  • a complete event
    event sequence를 성공과 함께 종료시키는 이벤트이다.
    다시 말해, Observable은 그것의 생명주기를 성공적으로 완수하고 어떠한 이벤트도 발생시키지 않을 것이라는 것을 말한다.
  • an error event
    Observable이 에러와 함께 종료되고 어떠한 이벤트도 발생시키지 않는 것을 말한다.

👆 그림을 한 번 보자.
여기서 파란색 박스들은 Observable에 의해 방출되는 next events에 해당한다.

✋ 참고로 필자는 Observable이라는 영단어를 읽을 때, 관찰 가능한 객체라고 생각하면 이해가 쉬웠다.

그리고 그림 오른쪽의 수직 선은 complete events를 나타낸다.
마지막으로 error event는 사진에는 없지만 타임라인 상에서 x로 나타날 것이다.

이것은 Observable이 방출할 수 있는 세가지의 가능한 이벤트들이다.
Rx에서는 어떠한 타입들도 방출할 수 있다.

현실 상황에 빗대어 더 자세한 아이디어를 얻기 위해 당신은 다음의 두 가지 개념을 배울 것이다.
finiteinfinite !


Finite observable sequences (유한한 옵저버블 시퀀스)

어떤 observable sequences는 0, 1, 혹은 그 이상의 값들을 방출하고 어떤 특정 시점에 완전히 종료되거나, 에러와 함꼐 종료된다.

안드로이드 앱에서는 인터넷에서 파일을 다운로드 받는 코드를 고려해볼 수 있다.

  • 처음, 다운로드 버튼을 누르고 들어오는 데이터를 관찰하기 시작한다.
  • 그 다음, 파일을 다운 받는 동안 여러 조각 파일들(chunks)들도 반복적으로 받게 된다.
  • 네트워크 연결이 유실되면, 다운로드는 멈출 것이고, time-out 에러를 낼 것이다.
  • 반대로, 파일을 모두 다운로드 받는 데에 성공한다면, success와 함께 완료될 것이다.
API.download(file = "http://www...")
  .subscribeBy(
	onNext = {
      	// append data to a file
    },
    onComplete = {
        // use downloaded file
	},
	onError = {
    	// display error to user
	} )

여기서 API.downloadObservable<String> 인스턴스를 반환하고, 이는 네트워크로부터 조각 데이터들을 String 타입으로 받았기 때문이다.

subscribeBy를 호출하는 것은 프로그래머가 observable, 즉 관찰 가능한 객체에게

이제부터 너를 구독하겠어 🫵

라고 말하는 것과 같다. 물론 그 표현들은 다음에 설명할 람다식에 나온다.

정확히 말하면 onNext 람다를 제공하면서 next events에 구독한다고 볼 수 있다.
다운로드 하는 것으로 예를 들면, 디스크에 저장되어 있는 임시 파일에 데이터를 계속해서 추가하는 것과 같다.

error eventonNext 람다를 통해 구독할 수 있는데, 경고창 같은 곳을 통해
Throwable.message를 던질 수 있다.

마지막으로, complete eventonComplete 람다를 통해 구독할 수 있는데, 여기서 프로그래머는 새로운 Activity를 시작한다거나 다운로드 완료된 파일을 보여준다거나 할 수 있다.


Infinite observable sequences (무한한 옵저버블 시퀀스)

파일 다운로드와 같은 활동들과 다르게, 단순히 무한한 시퀀스들도 존재한다.
종종 UI events들은 무한한 옵저버블 시퀀스들이라고 볼 수 있다.

예를 들어, 토클 버튼을 누르면서 스위칭에 반응하는 코드를 짠다고 생각해 보자.

  • 프로그래머가 OnCheckedChangedListener를 원하는 스위치에 달았다.
  • 그리고 OnCheckedChangedListener 람다 콜백을 제공하게 되는데, 이것은 isChecked 값을 바라보며 이를통해 앱 state를 적적하게 업데이트 할 것이다.

이러한 일련의 스위치 체크 상태 변화 시퀀스는 끝이 존재하지 않는다.
그렇기 때문에 처음 구독하는 시점에 초기값을 가지게 된다. ~(여기서는 on/off)~

switch.checkedChanges()
  .subscribeBy(
    onNext = { isOn ->
      if (isOn) {
        // toggle a setting on
	  } else {
        // toggle a setting off
	  }

checkedChanges() 메서드는 Observabe<Boolean> 객체를 반환해주는 CompoundButton의 확장함수라고 볼 수 있다.

여기서는 onError와 onComplete 인자들을 건너 뛰었는데, 
그 이유는 이러한 이벤트들을 observable이 방출하지 않는다. 
위의 스위치가 그 예이다.

Operators

ObservableSource<T>와 Observable 클래스의 구현체는 더 복잡한 로직을 구현할 수 있는 일련의 비동기 작업들을 추상화하는 여러 메서드들을 포함한다.

이러한 것들은 주로 decoupled 되어있고 composable하기 때문에 이러한 메서드들을
operators라고 부르게 되었다.

이러한 operators는 비동기 입력을 받고 어떠한 side effects도 발생시키지 않는다.
그리고 그들은 마치 퍼즐 조각들 처럼 잘 뭉치며, 큰 그림을 만들어가도록 동작한다.

(5 + 6) * 10 - 2

👆 다음의 수식을 예를 들어 보자.

분명하게 당신은 이 계산식을 통해 결과를 생각해 낼 수 있을 것이다.
비슷한 방식으로 Observable에 의해 방출된 데이터 조각 입력들을 side effects를 일으키는 최종 결과값을 낼 때까지 Rx operators에 적용시켜 결정적으로 입출력 과정을 도출해낼 수 있다.

switch.checkedChanges()
  .filter { it == true }
  .map { "We've been toggled on!" }
  .subscribeBy(
    onNext = { message ->
      updateTextView(message)
    }
)

checkChanges()가 true 혹은 false 값을 생산해낼 때마다, Rx는 filtermap 연산자를 방출된(생산된) 데이터에 적용한다.

filter는 오로지 true값만을 통과시킨다.
그리고 이러한 true값에 대해서, map operator는 Boolean 타입의 입력을 String 타입의 출력으로 변환해준다. (여기서는 "We've been toggled on!")
마지막으로, subscribeBynext event 결과에 구독을 하면서 갖고 있던 String 값을 통해 스크린 상에서 textView를 업데이트 하는 메서드를 호출한다.

참고로 이러한 연산자(operators)들은 굉장히 요소화(composable)되어 있기 때문에 
다양한 방식으로 연결 지을 수 있고, 다양한 결과를 도출해 낼 수 있다.

Schedulers

Scheduler는 자바나 코틀린 코드에서 볼 수 있는 ThreadPool과 비슷하다.

만약 당신이 next events들을 IO scheduler에서 관찰하기 시작할 때,
이것은 당신의 Rx 코드를 백그라운드 쓰레드 풀에서 동작하도록 만들어준다.
이 스케줄러는 보통 네트워크를 통해 파일을 다운로드 할 때나, 데이터베이스에 무언가를 저장할 때 사용된다.

TrampolineScheduler는 당신의 코드가 동시에 작동하도록 만들어준다.
ComputationScheduler는 분리된 쓰레드 풀에서 당신이 관찰하기로 한 무거운 컴퓨팅 작업들을 스케줄할 수 있도록 도와준다.

이러한 RxJava의 기능들 덕분에, 당신은 같은 subscription에 대해 다양한 작업들을 
다양한 스케줄러를 통해 관리할 수 있고, 최고의 성능에 도달할 수 있다. 👐

📌 App architecture (앱 아키텍쳐)

RxJava는 당신의 앱 아키텍쳐를 어떠한 방식으로도 변경할 수 없다.
이것은 대부분 이벤트들과 비동기 데이터 시퀀스들을 다룰 뿐이다.

Model-View-Controller(MVC)
Model-View-Presenter(MVP)
Model-View-ViewModel(MVVM)

이 중 어떠한 아키텍쳐를 선택해도 무방하다.

그런데 확실히 RxJavaMVVM 조합은 시너지가 있고, 나중에도 이 패턴으로 설명할 계획이다.
그 이유는 ViewModel이 Observable<T>를 노출하도록 도와주며, 이것을 통해 Activity의 UI 위젯에 직접 연결할 수 있고, 그것을 LiveData 객체(Android Jetpack)로 변환하고 구독할 수 있게 해준다.

📌 RxAndroid 그리고 RxBinding

RxJava는 Rx API의 공통된 구현체이다. 그래서 이것은 Android에 특화된 클래스들을 전혀 알지 못한다.

그래서 이러한 Android와 RxJava간의 갭 차이를 줄여줄 수 있는 라이브러리들이 있다.

첫 번째로 RxAndroid이다.
RxAndroid는 분명한 한가지 목적을 가지고 있는데, 그것은 Android의 Looper 클래스와 RxJava의 스케줄러 간 브릿지 역할을 해주기 위함이다.
당신은 이 라이브러리를 통해 간단하게 UI 쓰레드에서 Observable 결과물들을 받아서 뷰를 업데이트 할 수 있다.

두 번째 라이브러리는 바로 RxBinding이다.
RxBinding 라이브러리는 콜백 스타일의 뷰 리스너들을 observable한 객체로 변환해주는 다양한 메서드들을 제공해준다.

위에서 이미 이와 같은 코드를 봤을 것이다.

switch.checkedChanges()
  .subscribeBy(
    onNext = { boolean ->
      println("Switch is on: $boolean")
    }
)

여기서 checkedChanges()가 바로 RxBinding에서 제공하는 확장 함수이며, 이것은 Switch와 같은 CompoundButtonon/off states 스트림으로 변환해준다.

또한 RxBindingButton이나 EditText와 같은 컴포넌트들에도 바인딩을 제공해준다.


✨ 요약

  • RxJava는 자바 기반의 프로젝트(ex: 안드로이드)를 위해 Rx framework를 제공해주는 라이브러리이다.
  • RxJava는 코틀린 기반의 앱 프로젝트에도 사용될 수 있다.
  • RxKotlin 라이브러리는 RxJava위에 코틀린과 관련된 여러 유틸 함수들을 추가시킨 라이브러이다.
  • RxJava와 모든 Rx framework들은 비동기, 이벤트 기반의 코드들을 사용하기 위한 방법을 제공해준다.
  • RxJava는 반응형 시스템을 선언형 프로그래밍을 통해 설계하도록 도와준다.
  • RxJava에서 자주 쓰일 주요 요소들은 observables, operators, schedulers이다.
  • RxAndroid와 RxBinding은 RxJava를 안드로이드에서 사용하도록 도와주는 라이브러리들이다.

0개의 댓글