코루틴에 관한 글은 많지만, 맨처음 접하게되면 이건 뭔소리지 하는 경우가 한두번이 아니다. 개인적으로 정리 한번 진행하면서 나중에 다시 살펴봤을 때, 이전에는 이해하지 못했던 내용을 더 깊이 이해할 수 있는 상황을 만들고자 한다.
일단 기본적인 내용 파악은 Kotlin Coroutine 공식 페이지를 바탕으로 하고, Coroutine 예제는 github로 진행하고자 한다.
코루틴(Coroutine)
코틀린(Kotlin)의 "코"와 코루틴(Coroutine)의 "코"는 스펠링부터 다르듯이 코틀린에서만 사용 가능한 개념이 아니다. Co + Routine의 합성어로 단어 뜻 그대로 협동 루틴으로 이해하면 된다. 코(Co)는 with 또는 together 의미이고, 루틴(Routine)은 일련의 처리 과정이다.
프로그래밍 관점에서 보면 루틴은 일련의 처리과정을 묶어 놓은 함수라고 보면 되고, 코루틴은 함께(동시에) 실행되는 함수라고 이해하면 된다.
코틀린에서의 코루틴
코루틴하면 많이 보는 launch, async 등의 키워드는 코틀린 키워드가 아니며 표준 라이브러리도 아니다. JetBrains에서 개발한 코루틴용 라이브러리(kotlinx.coroutines)이다. 해당 라이브러리에 launch, async 등이 포함되어 있는 것이다.
코루틴 기초
이게 코루틴이다!
다음은 코루틴 기초 부분에 나오는 첫번째 코드이다.
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
맨처음 봤을 때는, 이게 다 뭐야. println만 알겠는데;; 생각했다. 일단 결과는 Hello가 먼저 보여지고, 1초 뒤에 World가 보여진다. 하나하나 살펴보면 다음과 같다.
launch : 코루틴 빌더로 코루틴을 만들어준다. 즉 나머지 코드와 함께 독립적으로 동시에 동작하는 새로운 코루틴을 만들어 준다. 따라서 Hello가 먼저 보여진다.
delay : suspending function으로 파라미터로 전달된 값만큼 코루틴을 일시정지한다. 일시정지는 기본 스레드를 block 시키지는 않고, 다른 코루틴 코드가 실행되도록 한다.
runBlocking : 앞서 설명한 launch와 같은 코루틴 빌더인데, 중요한 점이 runBlocking {} 중괄호 내부 코드가 동작할 때까지 메인 스레드를 block 시킨다. 위 예제에서 runBlocking {}를 통해 코루틴을 만들어주는데 내부 코드인 launch 코루틴과 마지막 println("Hello")가 모두 동작할 때까지 main 함수의 종료를 막는다. 일반적으로 스레드는 값비싼 리소스이고 스레드를 차단하는 것은 비효율적이기 때문에 자주 사용되지 않는다.
여기서 하나 더 살펴볼 것이 코루틴은 코루틴은 코루틴의 수명을 구분하는 특정 CoroutineScope에서만 동작한다는 점이다. 위 예제에서는 코루틴 빌더가 2개라 2개의 CoroutineScope를 지니고 있다.
위 코드를 보면 runBlocking과 launch의 중괄호 시작점을 보면 this: CoroutineScope로 hint 표시가 되어 있다. 각각 새로운 CoroutineScope를 지니고 있다는 뜻이다. 항상 모든 코루틴은 CoroutineScope에서 실행되어야 한다(runBlocking은 예외).
내부 루틴을 별도 함수로
fun main() = runBlocking { // this: CoroutineScope
launch { doWorld() }
println("Hello")
}
// this is your first suspending function
suspend fun doWorld() {
delay(1000L)
println("World!")
}
launch 코루틴 내부에 있던 내용을 따로 함수로써 새롭게 만들어 호출할 수 있다. suspend 함수는 suspend 함수 내부에서만 호출할 수 있으며, 코루틴에서 또한 호출할 수 있다. doWorld 함수 내부에 있는 delay 또한 suspend 함수이기 때문에 doWorld 함수 앞에 suspend 키워드를 추가해야만 한다. 코루틴 내부에서 suspend 함수를 만나면, 해당 코루틴을 호출한 스레드로 돌아가 기존 작업을 진행하다, 이후 다시 suspend 함수로 돌아와 작업을 재개한다.
Scope Builder
모든 코루틴은 Scope를 지녀야 한다. 앞에서 살펴본 launch나 이후에 살펴볼 async 또한 마찬가지다. 단 runBlocking은 예외적으로 top-level 함수로 정의되어 있어 Scope 내부에서 실행하지 않아도 된다. 이처럼 기본적으로 제공하는 코루틴 빌더를 제외하고 coroutineScope 빌더를 이용해서 Scope를 생성하는 것도 가능하다. 이 빌더를 이용해 생성하면, 내부의 모든 자식 코루틴이 완료될 때까지 완료되지 않는다. 얼핏보면 앞서 살펴본 runBlocking과 비슷하게 보이는데 차이점은, runBlocking은 현재 스레드를 block하지만, coroutineScope는 코루틴을 일시 중단한다는 것이다.
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
doWorld()
println("Done")
}
// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
println("Hello")
}
얼핏보면, "Done"->"Hello"->"World 1"->"World 2" 순서로 출력될 것 같지만 그렇지 않다.
Hello
World 1
World 2
Done
위처럼 결과가 출력되는데, doWorld 함수는 coroutineScope로 앞에서 설명했듯이, 내부 모든 자식 코루틴이 완료될 때까지 완료되지 않는다. 즉 doWorld 내부 루틴이 완료될 때까지, doWorld의 다음 내용인 Done이 출력되지 않는 것이다.
만약 다음처럼 coroutineScope 전에 다른 코루틴이 있다면 어떻게 될까?
// Sequentially executes doWorld followed by "Done"
fun main() = runBlocking {
launch {
delay(100L)
println("start")
}
doWorld()
println("Done")
}
// Concurrently executes both sections
suspend fun doWorld() = coroutineScope { // this: CoroutineScope
launch {
delay(2000L)
println("World 2")
}
launch {
delay(1000L)
println("World 1")
}
delay(500L)
println("Hello")
}
doWorld 앞에 launch 코루틴 빌더를 통해 start를 출력하고 있다. coroutineScope인 doWorld가 있기 때문에 start를 출력하는 코루틴은 doWorld가 완료되기 전까지 start가 출력되지 않을 것 같지만 실제로는 다음처럼 결과가 출력된다.
start
Hello
World 1
World 2
Done
이로써, coroutineScope는 자신보다 앞선 코루틴과 자기 자신을 포함한 코루틴이 완료되기 전까지 이후 진행을 하지 않는다고 보면 될듯 하다.
Job?!
만약 특정 코루틴 블럭이 완료되길 기다려야 하는 상황이 있다면 어떻게 해야할까? 이는 launch 코루틴 빌더가 반환하는 Job 객체를 이용할 수 있다.
val job = launch { // launch a new coroutine and keep a reference to its Job
delay(1000L)
println("World!")
}
println("Hello")
job.join() // wait until child coroutine completes
println("Done")
Hello
World!
Done
Hello->Done->World 순서가 아니라, launch 코루틴 빌더의 반환값은 job의 join 메서드를 이용해 job으로 반환된 코루틴이 완료되기를 기다릴 수 있다.
코루틴은 스레드보다 가볍다
코루틴과 스레드는 비슷한 점이 많다. 스레드보다 코루틴의 가장 큰 장점은 가볍게 사용 가능하다는 것이다. 스레드의 경우 코루틴처럼 동시 실행이 가능하도록 로직을 구현할 수 있지만, 스레드의 경우는 Context Switching으로 인한 리소스 소비가 심하다. 코루틴은 리소스에 대한 고민없이 사용할 수 있다는 점만 기억해두면 될듯 하다.
fun main() = runBlocking {
repeat(100_000) { // launch a lot of coroutines
launch {
delay(5000L)
print(".")
}
}
}
위 코드는 100000만개의 코루틴 블록을 실행시키는 코드이며, 매우 적은 메모리를 소비한다. 해당 내용을 스레드로 변경하여 Thread.sleep으로 작성한다면 OutOfMemory 에러가 발생할 것이다.
일단 이것만 알아두자
- 코루틴은 동시에 실행되는 협동 루틴
- 코루틴은 Scope에서 실행 가능(runBlocking 제외)
- 코루틴을 만들어주는 코루틴 빌더는 launch, async, runBlocking, coroutineScope.
'Android' 카테고리의 다른 글
[AOS] 코틀린(Kotlin) 코루틴 채널(Channel) (0) | 2023.02.09 |
---|---|
[AOS] 코틀린(Kotlin) 코루틴 취소(Coroutines cancel) (0) | 2023.02.06 |
[AOS] ViewModel (0) | 2023.01.19 |
[AOS] ContentProvider (feat. Room) (0) | 2023.01.16 |
[AOS] Room 로컬 데이터베이스 (0) | 2023.01.10 |