코틀린 코루틴을 공부하며 정리한 글입니다.
혼자 공부하고 정리한 내용이며, 틀린 부분은 지적해주시면 감사드리겠습니다 😀
핫 데이터 소스
핫 데이터 소스는 원소를 바로 생성(Eager)한다. 일반적으로 우리가 흔히 사용하는 List
를 생각하면 편할 것이다. 리스트 원소를 지정한 뒤, 여러 중간 연산(메소드 체이닝)을 걸면, 해당 연산이 바로바로 진행된다.
fun main() {
val list = listOf(1, 2, 3, 4, 5)
.onEach { println("onEach $it") }
.map { it * 10 }
println("=======")
println("Call List = $list")
}
onEach 1
onEach 2
onEach 3
onEach 4
onEach 5
=======
Call List = [10, 20, 30, 40, 50]
콜드 데이터 소스
콜드 데이터 소스는 요청이 있을 때만 원소를 생성(Lazy)한다. list
와는 다르게, 중간 연산이 바로 진행되지 않고, 해당 변수에 접근을 하여, 종단 연산을 할 때, 결과가 실행되는 것을 알 수 있다.
fun main() {
val list = sequenceOf(1, 2, 3, 4, 5)
.onEach { println("onEach $it") }
.map { it * 10 }
println("=======")
println("Call List = ${list.toList()}")
}
=======
onEach 1
onEach 2
onEach 3
onEach 4
onEach 5
Call List = [10, 20, 30, 40, 50]
여기서 중요한 것은 바로 종단 연산이다. 만약 list.toList()
가 아닌 list
만 출력하게 된다면, 다음과 같은 결과가 나오게 된다.
=======
Call List = kotlin.sequences.TransformingSequence@f2a0b8e
핫 데이터 소스와 콜드 데이터 소스의 차이
핫 데이터 소스와 콜드 데이터 소스의 가장 큰 차이는 바로 중간 연산의 실행 시점이다.
fun main() {
val list = listOf(1, 2, 3)
.onEach { println("onEach $it") }
.map {
println("map $it")
it * 10
}
println("=======")
println("Call List = ${list}")
}
onEach 1
onEach 2
onEach 3
map 1
map 2
map 3
=======
Call List = [10, 20, 30]
위 결과처럼, 핫 데이터 소스인 컬렉션의 리스트는 지정한 중간 연산마다 모든 원소를 처리하고, 다음 중간 연산을 처리하는 것을 알 수 있다. 이는, onEach
의 구현을 보면 알 수 있다.
public inline fun <T, C : Iterable<T>> C.onEach(action: (T) -> Unit): C {
return apply { for (element in this) action(element) }
}
return apply { ... }
은 사실상 return this.apply{ ... }
와 동일하다. 즉, 현재 컬렉션에 담겨있는 모든 원소를 for
문을 통해 처리하고 다시 현재 컬렉션을 반환한다. 그렇다면, 콜드 데이터 소스인 sequence
는 어떻게 동작할까?
fun main() {
val list = sequenceOf(1, 2, 3)
.onEach { println("onEach $it") }
.map {
println("map $it")
it * 10
}
println("=======")
println("Call List = ${list.toList()}")
}
=======
onEach 1
map 1
onEach 2
map 2
onEach 3
map 3
Call List = [10, 20, 30]
콜드 데이터 소스인 sequence
는 각 원소를 꺼내올 때마다, 개발자가 지정한 중간 연산을 하나씩 거치는 것을 알 수 있다. 어떻게 이렇게 동작하는지 살펴보자.
public fun <T> Sequence<T>.onEach(action: (T) -> Unit): Sequence<T> {
return map {
action(it)
it
}
}
컬렉션과 달리, sequence
는 원소를 필요할 때마다 하나씩 지연 처리하여 가져오는 방식이다. 각 원소는 호출된 중간 연산(onEach
, map
)을 순서대로 거치며 최종적으로 연산 결과에 도달한다. 그렇다면 이 지연 처리는 어떻게 작동하는 것일까?
public fun <T> Sequence<T>.toList(): List<T> {
val it = iterator()
if (!it.hasNext())
return emptyList()
val element = it.next()
if (!it.hasNext())
return listOf(element)
val dst = ArrayList<T>()
dst.add(element)
while (it.hasNext()) dst.add(it.next())
return dst
}
바로 toList()
라는 종단 연산을 호출할 때 지연 처리가 시작된다. toList()
함수 내부에서는 it.next()
를 통해 원소를 하나씩 가져오는데, 이때 각 원소는 sequence
에 지정된 중간 연산(onEach
, map
)을 차례로 통과한다. 이를 통해 시퀀스가 각 원소를 하나씩 지연 평가하며, 최종적으로 리스트 형태로 반환된다.
또한, list
는 중간 연산을 적용한 값을 계속해서 갖고 있지만, sequence
는 종단 연산을 할 때마다, 계속해서 중간 연산을 처리해 값을 만들어낸다.
fun main() {
val list = listOf(1, 2, 3)
.onEach { println("onEach $it") }
.map { it * 10 }
println("=======")
println("Call List = ${list}")
println("Call List = ${list}")
}
onEach 1
onEach 2
onEach 3
=======
Call List = [10, 20, 30]
Call List = [10, 20, 30]
fun main() {
val list = sequenceOf(1, 2, 3)
.onEach { println("onEach $it") }
.map { it * 10 }
println("=======")
println("Call List = ${list.toList()}")
println("Call List = ${list.toList()}")
}
=======
onEach 1
onEach 2
onEach 3
Call List = [10, 20, 30]
onEach 1
onEach 2
onEach 3
Call List = [10, 20, 30]
이처럼 sequence
는 공장과 같이 특정한 데이터를 만드는 과정을 정의한 레시피라고 보면 되고, 종단 연산을 실행하면, 그 때마다 데이터를 만들어내게 된다. 이는, 다음 게시물인 flow
를 이해하는데, 큰 도움이 될 것이다.
정리
핫 데이터 소스는 모든 원소에 대한 중간 연산을 바로바로 처리해 결과를 내지만, 콜드 데이터는 종단 연산을 호출했을 때, 각 원소마다 개발자가 지정한 중간 연산을 처리하게 된다.
'Book Study > [Kotlin] 코틀린 코루틴' 카테고리의 다른 글
[Coroutine] 플로우 (0) | 2024.12.15 |
---|---|
[Coroutine] 코루틴 빌더(2) (0) | 2024.08.22 |
[Coroutine] 코루틴 빌더(1) (0) | 2024.08.14 |
[Coroutine] delay (0) | 2024.08.08 |
[Coroutine] 중단 함수 (0) | 2024.08.06 |