프로젝트에서 UI Data의 data holding과 업데이트를 위해 StateFlow를 사용했는데, 반응형 프로그래밍에 대한 깊은 이해 없이 단순히 어려운 기술을 사용했다는 자기 만족 수준에 그쳤다는 것을 깨달았습니다. 프로젝트가 끝나고, 함수형 프로그래밍, 반응형 프로그래밍 전반, RxJava, Flow, StateFlow 등을 꼼꼼히 공부하며 해당스택을 이용할 줄만 아는 개발자가 아닌 원리를 이해한 개발자가 되고 싶었습니다💪
본 포스팅에선 반응형 프로그래밍을 "코드"로 살펴보기 전 개괄적인 개념을 먼저 이해하며 머리에 큰 그림을 그리고, 다음 편부터 RxJava를 실습해보겠습니다.
반응형 프로그래밍을 알기 위해선 "함수형 프로그래밍"에 대한 이해가 선행되어야 합니다. 반응형 프로그래밍과 함수형 프로그래밍은 단짝처럼 같이 사용되기 때문입니다!
함수형 프로그래밍은 하나의 프로그래밍 패러다임으로 정의되는 일련의 코딩 접근 방식이며, 자료처리를 수학적 함수의 계산으로 취급(자료처리를 함수를 이용해서 한다는 걸까)하고 상태와 가변 데이터를 멀리하는(전역변수를 멀리한다는 걸까) 프로그래밍 패러다임을 의미합니다.
함수형 프로그래밍은 기존 절차적 프로그래밍과 객체 지향형 프로그래밍과는 다른 새로운 방식입니다
절차적 프로그래밍 개념
절차적 프로그래밍은 Procedure(프로시저)를 이용하여 작성하는 프로그래밍 스타일입니다. 프로시저는 메소드, 루틴 등이 있습니다
절차적 프로그래밍은 순차적인 처리를 중요시 여기며, 프로그램 전체가 유기적으로 연결되도록 만드는 프로그래밍 기법입니다
절차적 프로그래밍의 장점
절차적 프로그래밍 단점
절차적 프로그래밍 종류
객체지향 프로그래밍 개념
소프트웨어의 급격한 발전으로 소프트웨어의 규모가 터지고 복잡해져 더 이상 절차지향 프로그래밍을 통해 소화할 수 없었습니다. 그 대안으로 객체지향 프로그래밍을 개발해 사용하게 됩니다!
객체지향 프로그래밍이란, 절차적 프로그래밍을 보완한 새로운 패러다임으로 프로그래밍에 필요한 속성과 메서드를 가진 클래스를 정의합니다. 정의된 클래스를 통해 각각의 객체를 생성해 객체들 간 유기적인 상호작용을 통해 로직을 구성하는 프로그래밍입니다.
객체지향 프로그래밍 특징
객체 지향 프로그래밍 장점
객체 지향 프로그래밍 단점
객체 지향 프로그래밍 종류
✔ 동일한 입력엔 항상 같은 값을 반환해야 하는 함수
✔ 함수의 실행이 프로그램의 실행에 영향을 미치지 않아야 하는 함수
✔ 함수 내부에서 인자의 값을 변경하거나 프로그램 상태를 변경하는 사이드 이펙트가 없는 것
var num=1
fun add(a){
return a+num
}
fun add(a,b){
return a+b
}
val answer=add(2,3)
✔ 함수형 프로그래밍에서의 데이터는 변하지 않는 불변성을 유지
✔ 데이터의 변경이 필요한 경우, 원본 데이터 구조를 변경하지 않고 그 데이터의 복사본을 만들어서 그 일부를 변경하고, 변경한 복사본을 사용해 작업을 진행
data class Person(
var name: String,
var age:Int
)
var person = Person("연아", 12)
fun increaseAge(person:Person) {
person.age = person.age + 1
return person
}
data class Person(
var name: String,
var age:Int
)
var person = Person("연아", 12)
fun increaseAge(person:Person) {
return Person(person.name, person.age + 1 )
}
✔ 명령형 프로그래밍은 무엇을 어떻게 할 것인가에 주목하고, 선언헌 프로그래밍은 무엇을 할 것인가에 주목
val numbers=mutableListOf(1,2,3)
fun multiply(numbers:MutableList<Int>, multiplier){
for(i in numbers.indices){
numbers[i]=numbers[i]*multiplier
}
}
위의 예시에서는 for문을 사용해서 배열의 각 요소에 multiplier 곱해주는 명령형 프로그래밍입니다
함수형 프로그래밍에서는 마찬가지로 if,switch,for 등 명령문을 사용하지 않고 함수형 코드로 사용해야합니다
fun multiply(numbers, multiplier):List<Int>{
return numbers.map{it*multiplier}
}
✔ 함수형 프로그래밍에서는 함수가 1급 객체가 됨
✔ 1급 객체의 특징
1. 변수나 데이터에 할당 할 수 있어야 함
//kotlikn
object Main {
@JvmStatic
fun main(args: Array<String>) {
val a = test
}
val test: () -> Unit = { println("kotlin") }
}
//java
public class java {
public static void test(){
System.out.println("java");
}
public static void main(String[] args) {
System.out.println("java");
// Object a = test;
}
}
2. 객체의 인자로 넘길 수 있어야 함
object Main {
@JvmStatic
fun main(args: Array<String>) {
function(test)
}
fun function(f: () -> Unit) {
f.invoke()
}
val test: () -> Unit = { println("kotlin") }
}
3. 객체의 리턴값으로 리턴 할수 있어야 함
object Main {
@JvmStatic
fun main(args: Array<String>) {
function()
}
fun function(): () -> Unit {
return { println("kotlin") }
}
}
function함수는 { println(“kotlin”) }, 즉 함수타입을 반환 합니다.
kotlin에서 함수는 변수나 data에 할당이 가능하며, 함수의 인자로 전달가능 하고, 함수의 리턴값으로도 사용 할 수 있습니다.
그렇기 때문에 kotlin의 함수는 1급 객체라고 할 수 있습니다. 반면 Java의 함수는 위 조건들을 만족하지 못하기 때문에 1급 객체라고 할수 없습니다.
반응형 프로그래밍을 하기 위해선 함수형 프로그래밍의 지원이 필요한데, 이때, 함수들을 직접 구현하기 보다 이미 만들어진 순수함수들(filter, map) 등을 주로 가져다 씁니다.
반응형 프로그래밍을 "코드"로 살펴보기 전, 개괄적인 개념을 먼저 이해하고, 다음 편부터 RxJava코드를 통해 실습해보겠습니다.
반응형 프로그래밍은 비동기적인 데이터 스트림을 이용한 프로그래밍 기법입니다. (데이터(=스트림=이벤트의 나열)가 변경->데이터를 계속 전달)
반응형 프로그래밍에서는 모든 데이터를 "스트림"으로 봅니다.
기본 베이스는 Observer Pattern입니다.
하나의 데이터 스트림을 감시(구독)하는 대상이 있다면, 데이터 스트림의 변화가 발생할 경우
변화 전파가 일어나는데 감시하는 대상은 이를 감지하여 관련 작업을 하게 됩니다
즉, 데이터가 변경될 때마다 관련된 로직을 일일히 호출하는 것이 아니라, 데이터 스트림이 존재하고 이를 구독하는 곳에서 변화에 따라 알아서 처리하는 하겠다는 것입니다.
반응형의 경우 모든 데이터를 스트림(시간순으로 발생하는 이벤트의 나열)으로 보고 스트림의 데이터가 변화되면 이를 전파하여 해당 데이터 스트림(이벤트의 나열)을 구독하고 있는 곳도 영향을 받습니다(상태변화의 흐름이 자동 전파)
명령형 프로그래밍의 경우 어떤 동작에 중점을 두고 데이터의 소비자는 데이터를 요청한 후 받은 결과값을 일회성으로 수신하고 매번 요청합니다=비효율적!!
반응형 프로그래밍의 경우 데이터 스트림의 변화에 중점을 두고 데이터를 발생하는 발행자는 새로운 데이터가 들어오면 데이터의 소비자에게 지속적으로 발행합니다
반응형의 장점은 여러개의 스트림을 조합하여 새로운 스트림을 만들어 낼 수 도 있으며,비동기 연산들 또한 하나의 스트림으로 조합할 수 있고, 비동기 연산을 보다 쉽게 처리할 수 있도록 해줍니다
ReactiveX
= 'Reactive eXtensions' 반응을 확장
= Rx
= "옵저버블 스트림으로 비동기 프로그래밍(실행흐름 안기다리고 바로 다음코드)을 하기 위한 API = 반응형 프로그래밍(모든 데이터를 스트림으로 보는)을 위한 API
= 데이터 처리 자체가 오래걸리는 작업이니까 비동기로 처리해야하고 데이터스트림으로 만들어서 처리하고 싶을 때 쓰는 라이브러리=RxJava
이것이 가능한 이유는 Rx는 각 플랫폼마다 지원하는 시리즈, 정확히는 라이브러리가 존재하기 때문입니다.
iOS 플랫폼에서 Rx를 사용할 수 있게 해주는 RxSwift라이브러리, Android 플랫폼에서 Rx를 사용할 수 있게 해주는 RxJava라이브러리가 있습니다
각 플랫폼의 개발 언어 앞에 Rx접두어를 붙인 이름의 라이브러리들을 만들어 놨기에 가능한 것입니다.
<퀘스트>
상황:
1. 오직 한 방향으로만 흐르는 강(stream)이 있습니다.
2. 물고기(value)는 강(stream)의 흐름방향으로 흘러갑니다.
3. 강에는 가끔 쓰레기(value)도 흘러갑니다.
목표:
'rx'라는 강에서 물고기를 건져, 회를 뜬다음, 팔아야 합니다.
당신이 한 일:
강에서 자동으로 물고기만 건져(filter) 회로 변환(map)하는 'A'시스템을 만들었습니다.
이 시스템은 사용자가 버튼을 누를 시(subscribe) 가동됩니다.
스트림은 주로 강으로 비유됩니다. 상식적으로 강은 한 방향으로만 흐르며, 우리가 무언가를 하지 않는 이상 아무 일도 생기지 않습니다. 이것이 강과 스트림을 비유하는 이유입니다
어떠한 이벤트 스트림이 있다고 해도, 그것만으로는 아무 일도 일어나지 않습니다. Observable도 강이라고 생각하시면 됩니다. 하지만 Observable은 강이지만, 특정한 강을 지칭할 때 사용한다고 생각하시면 됩니다. 다 같은 강이지만 한강, 낙동강처럼 특정한 강이 있죠? 스트림은 포괄적인 느낌이며, Observable은 스트림이지만, 특정 스트림을 의미한다고 생각하시면 됩니다.
'A'시스템을 가동하기 위해서는 버튼을 눌러야 합니다. 이 버튼을 누르는 행동인 subscribe 즉, 구독을 함으로써 시스템이 가동됩니다. 하지만 우리는 그냥 강이 아닌 'rx'라는 강에서 가동이 되야겠죠?
그래서 우리는 'A'시스템을 'rx'강에서 가동하기 위해, 'rx'라는 Observable을 구독해야 합니다. 강은 수없이 많습니다. 그 수많은 강 중에서 특정한 강(Observable)을 구독해야 시스템은 그 강에서 가동될 것입니다.그리고 Observable을 구독하지 않는 이상 아무 변화가 일어나지 않을 겁니다.
Operators는 스트림에 흐르는 값(value)을 가공하는 장치입니다. 위 퀘스트에서 우리는 물고기만 건져 회로 변환한다고 하였습니다. 장치(Operators)의 기능 중 물고기만을 건지게 해 주는 건 filter, 회로 변환하는 건 map. Operators안에 filter와 map과 같은 기능들이 있다고 생각하시면 됩니다.
이를 코드로 나타내면 아래와 같습니다(참조한 블로그에서 RxSwift로 예시를 들어주셨습니다 RxJava를 더 공부하고 RxJava코드로 변경하겠습니다! 조금만 기다려주세요😊)
import RxSwift
let disposeBag = DisposeBag()
let value = ["물고기", "쓰레기"] // 강에 흐르는 value
let rx = Observable.from(value) // Observable: value가 흐르는 'rx'강
rx
.filter { $0 == "물고기" } // Operators: 물고기만 건짐
.map { "\($0) 회" } // Operators: 물고기를 회로 만듦
.subscribe { print($0.element ?? "") } // Subscribe: 'rx'강을 구독
.disposed(by: disposeBag)
// 결과: 물고기 회
비유 출처) https://axe-num1.tistory.com/22
많은 블로그에서 RxJava와와 Coroutine을 비교합니다. 이 두 스택은 비동기 처리를 도와주는 부분은 동일합니다.
하지만 Asynchronous 처리를 어떠한 형태로 할 것인지가 다를 뿐입니다.
✔ RxJava
Observable pattern으로 구독
구독 후 들어오는 data를 stream 형태로 내려보냄
중간중간 데이터가 변환되는 걸 stream을 통해 확인 가능
.
✔ Coroutines
함수 호출을 하고, return이 오기 전까지 기다림
처리가 끝나 return 한 데이터를 가지고 다음 처리
Flow는 Coroutines 1.3에 정식으로 추가된 스펙(인터페이스)이고, 결국 RxJava의 Cold observable같은 기능을 해줍니다.
RxJava가 데이터 스트림작업을 비동기 처리로 하는걸 지원하는 라이브러리인 것 처럼 Coroutine Flow도 코루틴 상(비동기)에서 스트림 작업 지원을 위한 구현체입니다
즉 코루틴에서 데이터스트림(강 패러다임)만들고 싶으면 Flow를 사용해야합니다
결국 RxJava의 Cold observable을 구현하고, Data stream을 만들어준 부분인데, RxJava를 안다면 어렵지 않게 접근 가능합니다
샘플코드를 보겠습니다. for 문으로 1..3까지 emit 하는 간단한 코드입니다 foo() 함수의 collect이 호출되기 전까지는 동작하지 않는데, RxJava에서는 subscribe와 동일합니다
fun foo(): Flow<Int> = flow {
println("Flow started")
for (i in 1..3) {
delay(100)
emit(i)
}
}
fun main() = runBlocking<Unit> {
println("Calling foo...")
val flow = foo()
println("Calling collect...")
flow.collect { value ->
println(value)
}
println("Calling collect again...")
flow.collect { value ->
println(value)
}
}
click-> Flow 자세히 공부하고 다시 돌아오자!!
flow는 데이터의 흐름을 발생시키기만 할 뿐이라서 화면 돌리면 UI가 사용하는 데이터 초기화되어서 다시 API요청해서 flow흐르게 해야함 즉 데이터가 저장이 안됩니다
그래서 flow로 흐른 데이터를 viewmodel에서 마지막에 저장을 해두고 이 데이터를 UI가 보고있으면 뷰모델은 UI Controller보다 수명주기가 길기 때문에 flow가 끊겨도 UI가 이 데이터를 UI Data로 계속 들고있을 수 있음-> 근데 이 저장만을 위한 데이터를 만드는게 비효율적인 보일러 플레이트를 발생시킵니다.
흐르기도 하고=flow 데이터 홀더 역할도 해주면 안될까?하고 등장한게 StateFlow입니다 화면 재구성되어도 마지막 데이터를 홀딩하고 있어서 UI Data가 해당 데이터로 바로 업데이트 됩니다