안드로이드 앱을 개발하고 있는 여러분에게는 이런 의문점이 생길 것이다.
"나는 분명히 코틀린을 사용하고 있는데 왜 RxJava에 대한 글을 읽고 있지?"
좋은 질문이다!
RxJava는 코틀린이 주류 프로그래밍 언어로 인정받기 전인, 2013년도부터 존재해왔으며,
자바와 100% 호환 가능한 언어인 코틀린을 위한 RxJava를 처음부터 다시 만드는 것도
말이 안되는 일이였다.
그냥 기존에 있었던 RxJava 라이브러리를 활용하면 되는데 말이다!
그렇지만, RxJava가 코틀린을 위해 완전히 재작성 될 필요가 없다는 이유로 코틀린 언어의 장점을 수혜받지 못한다는 것은 또 말이 안되었다...
그래서 RxKotlin이 등장했다!
RxKotlin은 쉽게 말해 RxJava에 수많은 유틸과 확장 함수들을 추가함으로써 확장시킨 라이브러리라고 할 수 있다!
그럼 RxJava는 뭔데?
영어 본문을 해석하지 않고 일단 그대로 가져오겠다.
🤦 읽기만해도 복잡해보이지 않는가?
쉽게 풀어쓰자면, RxJava는 비동기 프로그램을 단순화하도록 도와주며,
이는 새로운 데이터에 반응하도록, 그리고 그것을 연속적, 고립된 방법으로 구현한다.
다시 말해, RxJava는 앱에서 일련의 비동기 이벤트들을 관찰하도록 하고,
그에 맞추어서 각각의 이벤트들에 반응한다.
참고로, 앱 상에서 유저에 의해 터치되는 탭 이벤트들과 그 결과를 비동기 네트워크 요청에
사용하는 것을 에로 들 수 있다.
다음은 일반적인 안드로이드 앱에서 일어날 수 있는 기능들이다.
얼핏보기에 이 모든 기능들은 동시에 일어날 수도 있을 것 같다.
키보드가 스크린상에서 펼쳐지는 애니메이션이 발생할 때마다,
그 애니메이션이 다 끝나기 전까지 앱에서 효과음을 계속 내고 있지 않는가?
안드로이드 운영체제는 당신에게 다양한 작업들을 각기 다른 쓰레드에서 수행하도록 도와주고, 기기의 CPU가 갖고 있는 각기 다른 코어에서 수행할 수 있도록 해준다.
구글은 당신으로 하여금 비동기 코드를 작성하는데에 도움을 주고자 몇가지 다양한 API를 제공한다.
물론 이것이 끝이 아니다.
Handler, JobScheduler, WorkManager, HandlerThread, Kotlin Coroutines 등이 있다.
안드로이드 개발 생태계에서 코틀린 코루틴은 점점 큰 비중을 차지해가고 있으며, 이런 상황 속에서 이 글을 보고 있는 당신은 아마도 아직도 RxJava를 공부해야 하는지 의문점이 들 수도 있다.
그러나 실제로, RxJava와 코루틴은 다른 추상화 레벨에서 동작한다.
코루틴은 쓰레딩에 경량적인 접근 방식을 제공하고 비동기 코드를 동기적인 방식으로 작성할 수 있도록 도와준다.
반면, Rx는 위에서 언급했던 예시처럼 주로 이벤트 기반 아키텍쳐(event-driven architecture)에서 사용되며, 반응형 앱을 만들 수 있도록 도와준다.
대부분의 일반적인 클래스는 비동기적으로 무언가를 수행하며, 모든 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는 그 진가를 발휘한다! 💪
State, Mutable State
State는 정확히 정의 내리기는 어렵다. state를 이해하기 위해서는 다음의 예시를 먼저 이해해야 한다.
예) 만약 당신이 노트북을 사용한다고 생각해보자. 며칠 동안 혹은 몇 주 동안은 사용하는데에 전혀 문제가 없다가, 갑자기 당신의 노트북이 맛탱이가 갔다고 쳐보자. 분명 하드웨어와 소프트웨어는 똑같은데, 변한 것은 state 뿐이다. 노트북을 재부팅하자마자, 똑같은 하드웨어와 소프트웨어의 조합인 당신의 노트북은 다시 잘 작동한다.
메모리 데이터, 디스크에 저장된 데이터, 유저 입력에 따른 모든 부산물들, 클라우드 서비스로 데이터를 최신화한 후에 생긴 모든 흔적 파일들이 당신 노트북의 state라고 할 수 있다.
Imperative Programming (명령형 프로그래밍)
명령형 프로그래밍은 프로그램의 state를 변경하는 명령들을 사용하는 프로그래밍 패러다임이라고 보면 된다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupUI()
bindClickListeners()
createAdapter()
listenForChanges()
}
다음의 코드들은 각각의 메서드들이 무엇을 하는지 말해주지 않는다.
더 혼란스러운 것은 저 메서드들이 올바른 순서로 호출되는가이다.
아마 어떤 이는 실수로 저 메서드들의 호출 순서를 바꾸고, 커밋을 할 수 있을 것이다.
한 프로그래머의 실수로 발생한 이러한 스와핑 때문에 앱이 다르게 동작할 수도 있다.
Side effects
Side effects는 현재의 스코프 바깥에서 일어나는 어떠한 state의 변화를 말한다.
다시말해 어떤 함수가 함수 안에 정의된 지역변수 이외에 어떤 state를 변경한다면, 그 함수는 side effect를 발생한다고 볼 수 있다.
Declarative code (선언형 코드)
명령형 프로그래밍에서 프로그래머는 의지에 따라 state를 변경할 수 있다.
함수형 프로그래밍은 이와는 정반대되는 개념이다.
함수형 코드에서는 어떠한 side effects도 발생시키지 않는다.
여기서 RxJava는 이러한 명령형 코드와 함수형 코드의 최고 장점만 골라서 합친다!
side effects 를 일으키지 않을 뿐더러, 함수형 코드는 선언형(declarative)이다.
다시말해, 코드는 how that encompasses the imperative way of programming 할 때가 아니라 what you want to do 에 초점을 둘 때, 선언적(declarative)이다고 볼 수 있다!
요약하면, reactive systems는 유저와 다른 이벤트들에 유연하고 일관성 있는 방식으로 반응한다.
👉 처음에 이 그림을 봤을 때 한줌의 미역 줄기인 줄 알았는데,
알고보니 이것은 전기뱀장어를 형상화한 것이라고 한다.
(Rx 프로젝트는 원래 Volta라고 불렸다.)
이제부터 본격적으로 observables, operators, schedulers에 대해서 자세히 설명하겠다.
Observable<T> 클래스는 Rx 코드의 기반을 제공해주는데,
불변성의 T 데이터를 "운반"할 수 있는 일련의 이벤트들을 비동기적으로 생산할 수 있는 기능이 그것이다.
쉽게 말하면, Observable은 어떤 클래스들로 하여금, 다른 클래스에서 emitted된 값들을
구독할 수 있도록 하는 것이다.
Observable<T> 클래스는 모든 종류의 이벤트들에 반응하여 실시간으로 앱의 UI를 변경할 수 있도록, 혹은 새롭게 들어오는 데이터를 가공할 수 있도록, 한 명 이상의 관찰자를 허용한다.
ObservableSource<T> 인터페이스(Observable<T> 클래스를 구현한)는 굉장히 간단하다.
Observable은 다음의 세가지 타입 중 아무거나 emit할 수 있다. (그러면 관찰자가 전달받음)
👆 그림을 한 번 보자.
여기서 파란색 박스들은 Observable에 의해 방출되는 next events에 해당한다.
✋ 참고로 필자는 Observable이라는 영단어를 읽을 때, 관찰 가능한 객체라고 생각하면 이해가 쉬웠다.
그리고 그림 오른쪽의 수직 선은 complete events를 나타낸다.
마지막으로 error event는 사진에는 없지만 타임라인 상에서 x로 나타날 것이다.
이것은 Observable이 방출할 수 있는 세가지의 가능한 이벤트들이다.
Rx에서는 어떠한 타입들도 방출할 수 있다.
현실 상황에 빗대어 더 자세한 아이디어를 얻기 위해 당신은 다음의 두 가지 개념을 배울 것이다.
finite와 infinite !
어떤 observable sequences는 0, 1, 혹은 그 이상의 값들을 방출하고 어떤 특정 시점에 완전히 종료되거나, 에러와 함꼐 종료된다.
안드로이드 앱에서는 인터넷에서 파일을 다운로드 받는 코드를 고려해볼 수 있다.
API.download(file = "http://www...")
.subscribeBy(
onNext = {
// append data to a file
},
onComplete = {
// use downloaded file
},
onError = {
// display error to user
} )
여기서 API.download는 Observable<String> 인스턴스를 반환하고, 이는 네트워크로부터 조각 데이터들을 String 타입으로 받았기 때문이다.
subscribeBy를 호출하는 것은 프로그래머가 observable, 즉 관찰 가능한 객체에게
이제부터 너를 구독하겠어 🫵
라고 말하는 것과 같다. 물론 그 표현들은 다음에 설명할 람다식에 나온다.
정확히 말하면 onNext 람다를 제공하면서 next events에 구독한다고 볼 수 있다.
다운로드 하는 것으로 예를 들면, 디스크에 저장되어 있는 임시 파일에 데이터를 계속해서 추가하는 것과 같다.
error event는 onNext 람다를 통해 구독할 수 있는데, 경고창 같은 곳을 통해
Throwable.message를 던질 수 있다.
마지막으로, complete event는 onComplete 람다를 통해 구독할 수 있는데, 여기서 프로그래머는 새로운 Activity를 시작한다거나 다운로드 완료된 파일을 보여준다거나 할 수 있다.
파일 다운로드와 같은 활동들과 다르게, 단순히 무한한 시퀀스들도 존재한다.
종종 UI events들은 무한한 옵저버블 시퀀스들이라고 볼 수 있다.
예를 들어, 토클 버튼을 누르면서 스위칭에 반응하는 코드를 짠다고 생각해 보자.
이러한 일련의 스위치 체크 상태 변화 시퀀스는 끝이 존재하지 않는다.
그렇기 때문에 처음 구독하는 시점에 초기값을 가지게 된다. ~(여기서는 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이 방출하지 않는다.
위의 스위치가 그 예이다.
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는 filter와 map 연산자를 방출된(생산된) 데이터에 적용한다.
filter는 오로지 true값만을 통과시킨다.
그리고 이러한 true값에 대해서, map operator는 Boolean 타입의 입력을 String 타입의 출력으로 변환해준다. (여기서는 "We've been toggled on!")
마지막으로, subscribeBy는 next event 결과에 구독을 하면서 갖고 있던 String 값을 통해 스크린 상에서 textView를 업데이트 하는 메서드를 호출한다.
참고로 이러한 연산자(operators)들은 굉장히 요소화(composable)되어 있기 때문에
다양한 방식으로 연결 지을 수 있고, 다양한 결과를 도출해 낼 수 있다.
Scheduler는 자바나 코틀린 코드에서 볼 수 있는 ThreadPool과 비슷하다.
만약 당신이 next events들을 IO scheduler에서 관찰하기 시작할 때,
이것은 당신의 Rx 코드를 백그라운드 쓰레드 풀에서 동작하도록 만들어준다.
이 스케줄러는 보통 네트워크를 통해 파일을 다운로드 할 때나, 데이터베이스에 무언가를 저장할 때 사용된다.
TrampolineScheduler는 당신의 코드가 동시에 작동하도록 만들어준다.
ComputationScheduler는 분리된 쓰레드 풀에서 당신이 관찰하기로 한 무거운 컴퓨팅 작업들을 스케줄할 수 있도록 도와준다.
이러한 RxJava의 기능들 덕분에, 당신은 같은 subscription에 대해 다양한 작업들을
다양한 스케줄러를 통해 관리할 수 있고, 최고의 성능에 도달할 수 있다. 👐
RxJava는 당신의 앱 아키텍쳐를 어떠한 방식으로도 변경할 수 없다.
이것은 대부분 이벤트들과 비동기 데이터 시퀀스들을 다룰 뿐이다.
Model-View-Controller(MVC)
Model-View-Presenter(MVP)
Model-View-ViewModel(MVVM)
이 중 어떠한 아키텍쳐를 선택해도 무방하다.
그런데 확실히 RxJava와 MVVM 조합은 시너지가 있고, 나중에도 이 패턴으로 설명할 계획이다.
그 이유는 ViewModel이 Observable<T>를 노출하도록 도와주며, 이것을 통해 Activity의 UI 위젯에 직접 연결할 수 있고, 그것을 LiveData 객체(Android Jetpack)로 변환하고 구독할 수 있게 해준다.
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와 같은 CompoundButton을 on/off states 스트림으로 변환해준다.
또한 RxBinding은 Button이나 EditText와 같은 컴포넌트들에도 바인딩을 제공해준다.