ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코루틴을 이해해보자! (기초 2편)
    Coroutines & Flow 2025. 7. 6. 20:10

     

    안녕하세요~! 오늘은 저번 1탄에 이어 2탄으로 코루틴에 대해서 더욱 자세히 알아보고자 합니다~!

    1. Thread.sleep 

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        launch {
            println("launch: ${Thread.currentThread().name}") // (3) launch: main@coroutine#2
            println("launch") // (4)
        }
        println("runBlocking: ${Thread.currentThread().name}") // (1) runBlocking: main@coroutine#1
        Thread.sleep(500) // 500ms = 0.5s
        println("runBlocking") // (2)
    }

     

    첫 번째로 알아볼 내용은 Thread.sleep 입니다. launch 이전에 runBlocking 코루틴 빌더가 만든 코루틴이 메인 스레드에 실행되는 동안 Thread.sleep 함수를 만나면 코루틴이 잠드는 것이 아닌 스레드 자체가 주어진 일정 시간동안 멈춥니다. 스레드가 멈췄으니, 스레드 위에서 실행되는 코루틴은 해당 시간동안 아무 것도 하지 못합니다. 또한, 스레드가 멈추어있는 동안 기존에 사용하던 코루틴은 다른 코루틴에게 스레드를 양보하지 않고 독점합니다. 그림으로 도식화하면 아래와 같습니다. 

    Thread.sleep

     

    전에 배웠던 delay 함수와 비교해보겠습니다. delay 함수는 스레드가 멈추지 않고, 해당 스레드를 사용하고 있던 코루틴이 잠들면서 다른 코루틴에게 스레드를 양보했습니다. 따라서 delay 는 non-blocking suspend 함수입니다. (여기서 non-blocking 의 기준은 코루틴이 아닌 스레드입니다.) 하지만 Thread.sleep 함수는 코루틴과 상관없이 스레드 자체를 멈추기 때문에 blocking 함수입니다. 설명한 내용을 표로 아래와 같이 정리해보았습니다. 

    Thread.sleep delay
    java.lang.Thread.sleep kotlinx.coroutines.delay
    blocking function non-blocking suspend function
    스레드를 멈춤 코루틴을 멈춤 
    사용 빈도 ↓ 사용 빈도 ↑
    자바 / 스레드 환경 코틀린 코루틴 환경

     

    java.lang.Thread.sleep 에서 유추할 수 있듯이, Thread.sleep 은 자바(JVM) 스레드 자체를 멈추는 함수코루틴이 나온 훨씬 이전부터 존재했습니다. 코루틴도 JVM 위에서 돌아가기 때문에 Thread.sleep 을 사용할 수 있지만, 비동기 프로그래밍이 목적인 코루틴 내에서는 sleep 은 거의 쓰이지 않습니다. 하지만 Thread.sleep 은 예전의 자바 코드, 코루틴 도입 이전의 레거시 코드에서 호환성을 위해서 사용될 수 있으며, 테스크 코드에서도 가끔 사용되기에 같이 알아둘 필요성이 있습니다.

     

    2. 3개의 코루틴에서 실행 순서 비교하기

    import kotlix.coroutines.*
    
    fun main() = runBlocking {
        launch {
            println("launch1: ${Thread.currentThread().name}") // (2) launch1: main@coroutine#2
            delay(1000L) // 1s
            println("launch1") // (6)
        }
        launch {
            println("launch2: ${Thread.currentThread().name}") // (3) launch2: main@coroutine#3
            println("launch2!") // (4)
        }
        println("runBlocking: ${Thread.currentThread().name}") // (1) runBlocking: main@coroutine#1
        delay(500L) // 0.5s
        println("runBlocking!") // (5)
    }

     

    이번엔 코루틴 빌더 중 launch 를 한개 더 늘려 3개의 코루틴 빌더에 의해 생성된 코루틴이 delay 함수를 통해 어떻게 실행되는지 살펴보겠습니다. runBlocking 코루틴 빌더가 만든 코루틴이 먼저 메인 스레드에서 실행되다가 delay 함수를 만나 0.5 초동안 잠이 듭니다. 이때, 스레드를 첫번째 launch 코루틴에게 양보하게 됩니다. 하지만 해당 코루틴 역시 delay 함수를 만나 1초동안 잠이 들면서 두번째 launch 코루틴에게 스레드가 양보됩니다. 코루틴이 실행되고 나서 다른 코루틴에게 스레드를 양보하게 되는데 첫번째 launch 코루틴 빌더의 코루틴은 아직 잠들어 있고, runBlocking 코루틴 빌더의 코루틴은 깨어난 상태입니다. 따라서, runBlocking 이 먼저 실행된 이후 첫번째 launch 에게 스레드가 양보되면서 실행이 끝납니다. 그림으로 도식화하면 아래와 같습니다. 

    3개의 코루틴 실행 순서

     

    3. 상위 코루틴과 하위 코루틴의 계층적 구조

    import kotlinx.coroutines.*
    
    fun main() {
        runBlocking {
            launch {
                println("launch1: ${Thread.currentThread().name}") // (2) launch1: main@coroutine#2
                delay(1000L) // 1.0s
                println("launch1!") // (6)
            }
            launch {
                println("launch2: ${Thread.currentThread().name}") // (3) launch2: main@coroutine#3
                println("launch2!") // (4)
            }
            println("runBlocking: ${Thread.currentThread().name}") // (1) runBlocking: main@coroutine#1
            delay(500L) // 0.5s
            println("runBlocking!") // (5)
        }
        print("finish!") // (7)
    }

     

    여기서는 (1) ~ (4) 는 아까와 동일하고, (5) 다음의 순서가 중요한데요. (5) 가 호출된 상태에서는 부모(상위) 코루틴인 runBlocking 의 실행은 전부 끝납니다. 이때 보니 자식(하위) 코루틴 중 두 번째 자식(launch2) 는 부모와 마찬가지로 실행을 전부 끝냈지만, 첫 번째 자식(launch1) 는 아직 실행을 전부 끝내지 못한 상태입니다. 이때, 부모(상위) 코루틴은 자식의 실행 완료와 상관없이 바로 finish! 코드를 호출할까요? 아닙니다. 부모(상위) 코루틴은 자식(하위) 코루틴을 끝까지 책임지기 때문에 자식(하위) 코루틴이 다 끝나기 전까지 종료되지 않습니다. 코틀린에서의 코루틴의 이러한 계층적 구조때문에 부모 코루틴이 캔슬되면 자식 코루틴도 캔슬되며, 자식 코루틴이 끝나기 전까지 부모 코루틴은 끝까지 기다립니다. 이러한 점에서, 코루틴이 관리하기가 비교적 용이합니다. 코틀린과 달리 다른 언어에선 코루틴이 이 같이 계층적인 경우가 많지 않습니다.

     

    이때, finish! 는 코루틴과 별개로 메인 스레드 위에서 실행됩니다. 코루틴과 관계없이 실행되는 코드가 있기에 기존에는 그림을 코루틴을 중점으로 그렸지만 이번엔 스레드 위주로 그려봤습니다. (나름 열심히 그려보았지만 더 나은 그림이 있으면 수정하겠습니다.😭)

    코루틴의 계층 구조

    4. suspend 함수

    suspend 함수, 아마 서버 통신을 하면서 많이들 사용하실법한 함수입니다. 저는 지금까지 suspend 함수의 의미를 제대로 모르고 서버 통신 코드를 작성하다가 코루틴을 배우고 나서야 그 의미를 조금씩 깨닫고 있습니다. 아래 코드를 봅시다.

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        launch {
            doThree()
        }
        launch {
            doOne()
        }
        doTwo()
    }
    
    suspend fun doTwo() {
        println("runBlocking: ${Thread.currentThread().name}") // (1) runBlocking: main@coroutine#1
        delay(500L) // 0.5s
        println("runBlocking") // (5)
    }
    
    suspend fun doThree() {
        println("launch1: ${Thread.currentThread().name}") // (2) launch1: main@coroutine#2
        delay(1000L) // 1.0s
        println("launch1") // (6)
    }
    
    fun doOne() {
        println("launch2: ${Thread.currentThread().name}") // (3) launch2: main@coroutine#3
        println("launch2") // (4)
    }

     

    기존에 한 곳에 모아둔 코드를 가독성 유지를 위해 함수로 분리하고 싶은 경우가 있습니다. 만약에 아래와 같이 함수를 분리했다고 칩시다.

    fun doThree() {
        println("launch1: ${Thread.currentThread().name}") // (2) launch1: main@coroutine#2
        delay(1000L) // 1.0s
        println("launch1") // (6)
    }

     

    이때 코드를 실행하면 다음과 같은 오류가 뜹니다.

    Suspend function 'delay' should be called only from a coroutine or another suspend function.

     

    이 말을 해석해보면, delay 와 같은 지연 함수(suspend function) 은 코루틴 내부나 다른 지연 함수 내에서만 호출이 되어야 한다는 것입니다. 그래서, doThree 라는 함수(fun) 을 지연 함수(suspend fun) 으로 바꾸어야합니다. delay 같은 지연 함수를 일반 함수에서 호출할 수 없습니다. 그림으로 도식화하면 다음과 같습니다. 아까 작성한 코드에서 suspend fun 으로 분리만 했기 때문에 다른 차이는 없습니다.

    suspend fun

     

    오늘은 2탄으로 Thread.sleep 함수, 3개의 코루틴에서의 실행 순서 비교, 코루틴의 계층적 구조, suspend fun 에 대해 알아보았습니다. 3탄으로 코루틴에 대한 더욱 다양한 내용을 알아보겠습니다. 감사합니다!

    'Coroutines & Flow' 카테고리의 다른 글

    코루틴을 이해해보자! (기초 3편)  (4) 2025.07.13
    코루틴을 이해해보자! (기초 1편)  (2) 2025.06.29
Designed by Tistory.