깨알 개념/Kotlin

[Kotlin] Coroutines - 동기 코드, 비동기 코드

interfacer_han 2024. 2. 8. 12:26

#1 동기 코드 vs 비동기 코드

#1-1 구분하기

코루틴을 제대로 사용하기 위해선 먼저, '동기 코드'와 '비동기 코드(= 코루틴 코드)'를 명확하게 구분할 줄 알아야 한다. 둘을 구분하는 기준은 쉽게 말하자면 작업이 순차적으로 실행되는 지의 여부다. 순차적이라는 것은, 이전 작업이 완료될 때까지 다음 작업이 실행되지 않음을 의미한다. 비동기 코드는 동기 코드가 아닌 코드다.

 

#1-2 비동기 코드의 예시

import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

suspend fun main() {
    println("Start")

    val myJob = CoroutineScope(Dispatchers.IO).launch {
        println("Coroutine code start")
        delay(2000)
        println("Coroutine code end")
    }

    var meaninglessNumber = 0
    for (i in 1..100000000) {
        meaninglessNumber += i
    }

    println("End")

    myJob.join()
}

/* 출력 결과
Start
Coroutine code start
End
Coroutine code end
*/

위 코드에 '비동기 코드'가 존재함은 분명하다. 코드가 써진 순서와 출력 결과가 비례하지 않기 때문이다. 즉, 순차적이지 않기 때문이다.

 

#2 스레드의 병렬 구조

#2-1 필연적 비(非)순차성

이 예시 도식도에서, 원래 스레드에 있는 A + B 출력은 불가능하다. 해당 시점에선 A의 긴 작업 시간 때문에 그 값이 뭔지 모르기 때문이다. 이와 같이 병렬 구조를 구성하면, 시차가 날 수 밖에 없다. 물론, 마법처럼 모든 타이밍이 딱딱 맞아 떨어질 수는 있겠지만, 그건 동전 100개를 던져서 전부 앞면이 나오지 않으리란 법은 없다는 말과 똑같은 소리다. 즉, 이러한 시차는 필연적이다. 그리고 시차를 극복하려면, 아직 완료되지 않은 스레드를 기다려야 한다. 멈춰(Suspend)야 한다는 말이다.

 

#2-2 Suspend를 추가한 도식도

#2-1을 수정해 Suspend하는 작업을 추가했다.

 

#2-3 비동기 코드에서 Suspend가 없는 경우

스레드 병렬 구조에서 Suspend는 분명 필연적이지만, 필수는 아니다. 예를 들어, 작업 A와 B를 출력하는 게 아니라 그냥 계산만 하고 마는 경우가 그렇다. 시차를 맞춰서 할 작업이 없다. 이런 경우에는 굳이 Suspend하지 않는다. 즉, Suspend는 병렬 구조에서 '필요'하지만, '필수'는 아니라는 이야기다.

 

#3 동기 코드의 '영역' vs 비동기 코드의 '영역

동기 코드 영역은 하나의 스레드에서 작업이 실행되며, 이전 작업이 완료될 때까지 다음 작업이 실행되지 않는 환경이다. 우리가 일상적으로 접해온 코드의 일반적인 영역이 그렇다. 반대로 비동기 코드 영역은 작업들이 순차성 없이 병렬적으로 실행될  있는 환경을 말한다. Coroutine은 비동기 코드를 구현할 때 쓰 도구지만, Coroutine이라고 반드시 비동기 코드인 것은 아니다 (참조).
 
'동기 코드'의 영역엔 '비동기 코드'를 사용할 수 없다. 어째서? '비동기 코드'는 코드를 실행(Run)하는 주체인 시스템(Kotlin의 경우엔 Kotlin 런타임)을 '기대'하게 만들기 때문이다. 작업이 비동기적으로 실행된다는 얘기는 시스템이 언제 그 코드의 결과를 받아볼 수 있는 지를 알 수 없다는 이야기다. 즉, '비동기 코드'는 시스템에게 값을 즉각적으로 내주지 않는다. 따라서 시스템은 언젠간 작업이 완료(launch)되거나 값을 전달(async, produce)해줄 거라고 '기대'만 할 수 있다. '기대'는 '기다림'이다. 기다림은 '간헐적인 멈춤'이다. 간헐적으로 멈추는 코드를 어떻게 (에러가 나지 않는 한) 절대 멈추지 않음을 보장하는 '동기 코드 영역'에 둘 수 있겠는가? 
 
반면, '비동기 코드'의 영역엔 '동기 코드'를 사용할 수 있다. 비동기 코드의 영역이라고 반드시 비동기 코드만 있을 필요가 없다. '간헐적 멈춰짐이 허용되는 곳'이지, '반드시 멈춰야하는 곳'이 아니기 때문이다.

 

#1-2의 예시 코드를 보면, 비동기 코드는 CoroutineScope라는 포장지에 둘러싸여 밖에 있는 동기 코드의 영역과 그 영역이 분명히 구분됨을 볼 수 있다.

 

#4 요약

병렬 구조는 필연적으로 非순차적(非동기적)이고, 따라서 Suspend를 요구할 수 있다.