본 글은 코틀린 리액티브 프로그래밍을 보고 공부한 내용입니다
1. 리액티브 프로그래밍의 소개
리액티브 프로그래밍이란 ?
-
데이터 스트림과 변경 사항 전파를 중심으로 하는 비동기 프로그래밍 패러다임
-
리액티브 프로그램의 간단한 동작 예시
1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
var num = 4
var isEven = isEven(num)
println("number is $isEven")
num = 9
println("number is $isEven")
}
fun isEven(n: Int): Boolean = ((n % 2) == 0)
- num에 새로운 값이 할당됐음에도 isEven이 여전히 참이다
- isEven이 num 변수의 변경사항을 추적하도록 설정되었다면 자동으로 false가 됨
함수형 리액티브 프로그래밍을 적용해야 하는 이유
- 콜백 지옥의 제거
- 오류 처리를 위한 표준 메커니즘
- 간결해진 스레드의 사용
- 간단한 비동기 연산
- 전체를 위한 하나, 모든 작업에 대해 동일한 API
- 함수형 접근
- 유지보수 가능하고 테스트 가능한 코드
리액티브 선언
-
네 가지의 리액티브 원리를 정의해 놓은 문서
-
응답성(Responsive)
- 시스템은 즉각 응답해야하며, 응답성있는 시스템은 신속하고 일관성 있는 응답 시간을 유지해 일관된 서비스 품질을 제공한다
-
탄력성(Resilient)
- 시스템에 장애가 발생하더라도 응답성을 유지해야한다
- 탄력성은 복제(replication), 봉쇄(containment), 격리(isolation), 위임(delegation)에 의해 이루어진다
- 장애는 각 컴포넌트 내부로 억제돼 각 컴포넌트들을 서로 격리시키는데, 그래서 하나의 컴포넌트에 장애가 발생하더라도 전체 시스템에 영향을 끼치지 못하게 된다
-
유연성(Elastic)
- 리액티브 시스템은 작업량이 변하더라도 그 변화에 대응하고 응답성을 유지해야 한다
- 리액티브 시스템은 상용 하드웨어 및 소프트웨어 플랫폼에서 효율적인 비용으로 유연성을 확보한다
-
메시지 기반(Message driven)
- 탄력성의 원칙을 지키려면 리액티브 시스템은 비동깆거인 메시지 전달에 의존해 컴포넌트들 간의 경계를 형성해야한다
- 이 네 가지 원칙을 모두 구현함으로써 시스템은 신뢰할 수 있고 응답성 있다고 말할 수 있는, 즉 리액티브 시스템의 특징이다
RxJava의 푸시 메커니즘과 풀 메커니즘 비교
-
RxKotlin은 전통적인 프로그램에서 사용되는 반복자(Iterator) 패턴의 풀 메커니즘 대신 푸시 메커니즘의 데이터/이벤트. 시스템으로 대표되는 옵저버블 패턴을 중심으로 작동한다
내가 공부했던 디자인 패턴 중 하나
-
ReactiveX의 주요 구성요소는 옵저버블(Observables)이다
-
위에서 본 예제를 리액티브한 방법으로 수정해 보자
1
2
3
4
5
6
7
8
9
10
11
12
13
fun main(args: Array<String>) {
var subject: Subject<Int> = PublishSubject.create()
subject.map {
isEven(it)
}.subscribe {
println("the number is ${(if (it) "Even" else "Odd")}")
}
subject.onNext(4)
subject.onNext(9)
}
fun isEven(n: Int) = (n % 2) == 0
- map과 subject라는게 나왔다(책에서는 나중에 설명한다고 함)
- subject에 숫자를 통지하면 map 내의 메서드를 호출하고, 차례대로 메서드의 반환값과 함께 subscribe 내의 함수가 호출되는 구조이다
2. 코틀린과 RxKotlin을 사용한 함수형 프로그래밍
함수형 프로그래밍이란
-
불변의 데이터를 사용한 수학적인 함수의 평가를 통해 프로그램을 구조화하는 동시에 상태 변화를 방지한다
-
작고 재사용 가능한 선언적인 함수의 사용을 권장하는 선언적인 프로그래밍 패러다임이다
-
함수형 프로그래밍은 람다, 순수함수, 고차함수, 함수유형, 인라인 함수 같은 몇가지 새로운 개념으로 구성된다
람다 표현식
- 람다, 람다식은 일반적으로 이름이 없는 익명 함수를 의미한다
1
2
3
4
5
6
7
8
9
10
fun main(args: Array<String>) {
val sum = { x: Int, y: Int ->
x + y
} // 람다식 #1
println("Sum : ${sum(12, 14)}")
val anonymousMult = { x: Int ->
(Random().nextInt(15) + 1) * x
} // 람다식 #2
println("random output ${anonymousMult(2)}")
}
- 위의 두 람다식은 실제로 함수이면서 이름이 없기 때문에 익명 함수라고도 불린다
순수 함수
- 함수의 반환값이 인수/매개 변수에 전적으로 의존하면 이 함수를 순수 함수라고 한다
1
2
3
4
5
6
7
8
9
fun main(args: Array<String>) {
println("pure func square = ${square(3)}")
val qube = { n: Int ->
n * n * n
} // #1
println("lambda pure fun qube = ${qube(3)}")
}
fun square(n: Int) = n*n // #2
- #1은 람다, #2는 이름이 있는 함수
- 값 3을 어떤 함수에 n번 전잘하면 매번 동일한 값이 반환되기 때문에 순수함수는 부작용(side effect)이 없다
고차 함수(high-order functions)
- 함수를 인자로 받아들이거나 반환하는 함수를 고차함수라고 부른다
1
2
3
4
5
6
7
8
9
10
11
12
fun main(args: Array<String>) {
highOrderFunc(12, { a: Int -> isEven(a)})
highOrderFunc(19, { a: Int -> isEven(a)})
}
fun highOrderFunc(a: Int, validityCheckFunc: (a: Int) -> Boolean) {
if (validityCheckFunc(a)) {
println("a $a is Valid")
} else {
println("a $a is Invalid")
}
}
- Main 함수 내에서 highOderFunc 함수를 호출하는 시점에 런타임으로 validityCheckFunc 함수를 정의
인라인 함수
- 함수는 이식 가능한 코드를 작성하는 좋은 방법이지만 함수의 스택 유지 관리 및 오버 헤드로 인해 프로그램 실행 시간이 늘어나고 메모리 최적화를 저하시킬 수 있다
- 인라인 함수의 사용을 함수형 프로그래밍에서 이런 난관을 피할 수 있는 좋은 방법이다
1
2
3
4
5
6
7
fun main(args: Array<String>) {
for (i in 1..10) {
println("$i Output ${doSomeStuff(i)}")
}
}
inline fun doSomeStuff(a: Int = 0) = a + (a * a)
- 함수 정의를 호출할 때마다 그것을 인라인으로 대체할 수 있도록 컴파일러가 지시할 수 있다
- 인라인으로 선언하게되면 함수 호출이 함수 내부의 코드로 교체되는데, 함수 선언으로 얻는 자유는 지키며 동시에 성능도 향상시킬 수 있다
예제
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
/**
* sample code 1
*/
class ReactiveCalculator(a: Int, b: Int) {
internal val subjectAdd: Subject<Pair<Int, Int>> = PublishSubject.create()
internal val subjectSub: Subject<Pair<Int, Int>> = PublishSubject.create()
internal val subjectMult: Subject<Pair<Int, Int>> = PublishSubject.create()
internal val subjectDiv: Subject<Pair<Int, Int>> = PublishSubject.create()
internal val subjectCalc: Subject<ReactiveCalculator> = PublishSubject.create()
internal var nums: Pair<Int, Int> = 0 to 0
init {
nums = Pair(a, b)
/* add */
subjectAdd.map({
it.first + it.second
}).subscribe({
println("add = $it")
})
/* subtract */
subjectSub.map({
it.first - it.second
}).subscribe({
println("sub = $it")
})
/* multiply */
subjectMult.map({
it.first * it.second
}).subscribe({
println("mul = $it")
})
/* divide */
subjectDiv.map({
it.first / (it.second * 1.0)
}).subscribe({
println("div = $it")
})
subjectCalc.subscribe({
with(it) {
calculateAddition()
calculateSubtractions()
calculateMultiplication()
calculateDivision()
}
})
subjectCalc.onNext(this)
}
fun calculateAddition() {
subjectAdd.onNext(nums)
}
fun calculateSubtractions() {
subjectSub.onNext(nums)
}
fun calculateMultiplication() {
subjectMult.onNext(nums)
}
fun calculateDivision() {
subjectDiv.onNext(nums)
}
}
- 위의 예제는 많은 subject와 구독자가 있는데 이를 클래스의 구독자만으로 작업을 끝내는 구조로 수정하고 최적화 할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import io.reactivex.subjects.PublishSubject
import io.reactivex.subjects.Subject
/**
* sample code 2
*/
class ReactiveCalculator(a: Int, b: Int) {
val subjectCalc: Subject<ReactiveCalculator> = PublishSubject.create()
var nums: Pair<Int, Int> = 0 to 0
init {
nums = Pair(a, b)
subjectCalc.subscribe({
with(it) {
calculateAddition()
calculateSubtractions()
calculateMultiplication()
calculateDivision()
}
})
subjectCalc.onNext(this)
}
inline fun calculateAddition() = nums.first + nums.second
inline fun calculateSubtractions() = nums.first - nums.second
inline fun calculateMultiplication() = nums.first * nums.second
inline fun calculateDivision() = nums.first / nums.second
}
코루틴
- 스레드와 같이 비동기식, 논브로킹 코드를 작성하는 새로운 방법
- 더 간단하고 효율적이며 경량의 솔루션
- 코루틴과 RxKotlin의 스케줄러의 내부 구조가 동일하다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.concurrent.TimeUnit
import kotlin.system.measureTimeMillis
suspend fun longRunningTsk(): Long {
val time = measureTimeMillis {
println("Wait")
delay(2, TimeUnit.SECONDS)
println("Delay over")
}
return time
}
fun main(args: Array<String>) {
// 1
runBlocking {
val exeTime = longRunningTsk()
println("Execution Time is $exeTime")
}
}
- #1
- longRunningTsk 함수가 완료될 때까지 프로그램을 대기 상태로 만든다
- 코드에 따르면 메인 스레드가 대기하게 되는데, 비동기 작업을 위해서 코드를 수정하면 된다
1
2
3
4
5
fun main(args: Array<Sring>) {
val time = async(CommonPool) { longRunningTsk() }
println("print after async")
runBlocking { println("printing time ${time.await()}") }
}
- Async 코드블록은 코루틴 컨텍스트에서 비동기적으로 블록 내부의 코드를 실행한다
함수형 프로그래밍 : 모나드
모나드(Monad)
순서가 있는 연산을 처리할 때 사용되는 디자인 패턴으로 부작용을 관리하기 위해 함수형 프로그래밍 언어에서 사용된다
- 모나드는 값을 캡슐화하고 추가 기능을 더해 새로운 타입을 생성하는 구조체라고 설명된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fun main(args: Array<String>) {
val maybeValue: Maybe<Int> = Maybe.just(14)
maybeValue.subscribe({
// onSuccess
println("Completed with value $it")
}, {
// onError
println("Error $it")
}, {
// onComplete
println("Completed Empty")
})
val maybeEmpty: Maybe<Int> = Maybe.empty()
maybeEmpty.subscribe({
// onSuccess
println("Completed with value $it")
}, {
// onError
println("Error $it")
}, {
// onComplete
println("Completed Empty")
})
}
- 여기서
Maybe
는 모나드로서 Int 값을 캡슐화하고 추가 기능을 제공한다 - 모나드인 Maybe는 값을 포함할수도 있고 포함하지 않을 수도 있으며, 값 또는 오류 여부에 관계없이 완료된다
- 그래서 오류가 발생했을 때는 onError가 호출된다
- 여기서 주의해야 할것은 onError, onSuccess, onComplete 세가지 메서드가 모두 터미널 메서드인데 세가지 메서드 중 하나는 모나드에 의해 호출되지만, 다른 것은 호출되지 않음을 의미한다