ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 코루틴을 이해해보자! (기초 1편)
    Coroutines & Flow 2025. 6. 29. 21:32

     

    안녕하세요 6월 마지막 주말이네요. 벌써 7월이 다가오고 날씨도 요즘 많이 후덥지근해요. 저는 아이스크림을 좋아해서 요즘 거의 맨날 1일 1아이스크림 하고 있어요🍦오늘은 코루틴에 대해서 알아보겠습니다!

     

    코루틴을 배워야 하는 이유?

    혹시 앱을 사용하다가 화면이 그대로 멈추고 어떠한 클릭에도 반응하지 않아서 짜증났던 경험이 있으셨나요?

    이 현상은 메인 스레드라고 불리는 UI 스레드가 다른 요인에 의해 멈추어서 나타나는 것인데요. 네트워크 요청과 같은 비교적 무거운 작업을 메인 스레드에 하게 되면 앱이 멈추는 경우가 발생할 수 있습니다. 이를 해결하기 위해서는 무거운 작업을 UI 스레드가 아닌 백그라운드에서 처리해야 하는데, 이때 필요한 개념이 바로 코루틴(Coroutine) 입니다. 따라서 안드로이드 앱이 끊기지 않고 부드럽게 동작하게 만들기 위해서는 코루틴(Coroutine) 은 반드시 공부해야 할 필수 개념입니다. 

     

    스레드와 코루틴 의미

    먼저 의미부터 짚고 넘어가겠습니다! 스레드(Thread) 는 운영체제(OS) 가 관리하는 실제 작업 단위로, 코루틴보다 무거운 단위입니다. 메인 스레드(UI 스레드), 백그라운드 스레드 등이 있습니다. 코루틴(Coroutine) 스레드 위에서 동작하는 가상의 작업 단위입니다. 하나의 스레드 안에 여러 개의 코루틴이 실행될 수 있습니다. 스레드보다 비교적 가벼운 단위입니다. 

     

    첫 번째 코루틴 빌더, runBlocking 내부에서의 현재 스레드

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        println(Thread.currentThread().name) // main @coroutine1
    }

     

    코루틴의 아주 기초적인 개념부터 하나씩 배워나가겠습니다.  먼저 해당 함수를 실행하면 main @coroutine1 이라는 결과가 나옵니다. 이는 현재 스레드의 이름이 바로 메인 스레드(UI 스레드) 임을 의미합니다. 옆에 @coroutine1 이 붙어있네요. runBlocking 은 코루틴을 만드는 함수로, '코루틴 빌더' 라고 부릅니다. 코루틴을 생성하는 동시에 해당 코드 블록이 수행을 다 완료할 때까지 다음 코드를 수행하지 못하게 막는 '동기성' 을 가지고 있습니다. 막는다는 블로킹(Blocking) 의 의미를 여기서 이해할 수 있습니다. 

     

    runBlocking 의 수신 객체 this

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        println(this) // "coroutine#1" : BlockingCoroutine{Active}@3930015a
    }

     

    thisrunBlocking 내부의 수신 객체(Receiver) 입니다. 결과값에서 수신 객체가 코루틴인 것을 확인할 수 있는데, 이를 통해 runBlocking 이 다시 한번 코루틴 빌더임을 알 수 있습니다. BlockingCoroutine 은 runBlocking 이 만든 코루틴 객체의 타입입니다. 다시 말하면, 스레드를 점유하여 실행이 끝날 때까지 외부 실행을 막는 특수 코루틴 객체입니다. 뒤에 붙은 Active 는 해당 코루틴이 현재 실행 중임을 의미합니다. @3930015a 는 코루틴 객체의 고유 메모리 해시 주소입니다. 수신 객체는 람다의 확장(Extension Lambda) 으로, 코드 블록 내부에서 코루틴의 모든 기능을 확장된 것처럼 사용할 수 있습니다. 

     

    coroutineContext 란?

    import kotlinx.coroutines.*
    
    fun main() = runBlocking {
        println(coroutineContext) // [CoroutineId(1), "coroutine#1": BlockingCoroutine{Active}@1ff8b8f, BlockingEventLoop@387c703b]
    }

     

    coroutineContext코루틴 빌더의 수신 객체(=코루틴)에 대한 정보 집합체를 의미합니다. 첫 번째로, CoroutineId(1) 은 코루틴의 고유 ID 번호입니다. 1을 통해 첫 번째 코루틴임을 알 수 있습니다. 두 번째는, 코루틴 객체 자체의 정보입니다. 세 번째는, 이벤트 루프 타입과 해시 값입니다. 이벤트 루프는 코루틴 작업을 큐(queue) 에 넣고, 순서대로 작업을 실행하는 내부 작업자입니다.

     

    두번째 코루틴 빌더, launch

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

     

    runBlocking 이라는 코루틴 빌더를 배웠으니 이번엔 또 다른 새로운 코루틴 빌더를 배워볼까요? 바로 launch 입니다. launch 는 코루틴 빌더로 새로운 코루틴을 만들고, 즉시 실행되는 runBlocking 과 달리 바로 실행되지 않고 큐(queue) 에 넣어 비동기적으로 실행을 예약합니다. 나중에 이벤트 루프가 실행해줍니다. 결과를 해석하면, 먼저 runBlocking 이 만든 첫번째 코루틴 작업이 메인 스레드에서 먼저 실행됩니다. 이후 작업이 끝나면, launch 가 만든 두번째 코루틴 작업이 메인 스레드에서 실행되는데요. 1개의 메인 스레드 안에서 2개의 코루틴이 순서대로 실행되는 구조입니다. 이때, runBlocking 은 자신이 실행되고 나서 종료하지 않고 launch 코드 블록이 끝날 때까지 기다립니다

    runBlocking, launch 실행 흐름

     

    현재 코루틴을 잠재우는 delay 메소드

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

     

    runBlocking 코루틴 빌더가 만든 코루틴 작업이 먼저 메인 스레드에서 실행되는데요. 이때 delay 함수를 통해 현재 스레드에서 수행되는 코루틴을 일정 시간동안 잠재울 수 있습니다. 인자로는 밀리세컨드 단위의 시간을 지정할 수 있는데, 1000 ms(밀리세컨드) 는 1s(초) 입니다. 따라서 500L (=500ms) 는 0.5 초를 의미합니다. 0.5초 동안 runBlocking 내부 코루틴이 잠이 들면, 메인 스레드는 launch 내부 코루틴에게 양보됩니다. launch 에서 코루틴 작업이 완료되면 다시 메인스레드를 runBlocking 이 사용하게 됩니다. 

    delay 를 통한 스레드 양보

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

     

    조금 더 변형을 해보겠습니다. 메인스레드에서 runBlocking 의 1번 코드가 실행된 후에 delay 를 통해 0.5초 동안 첫 번째 코루틴이 잠듭니다. 메인 스레드가 launch 에게 양보되어 2번 코드가 실행되고 이 역시 delay 를 통해 두 번째 코루틴도 잠듭니다. 이때, 메인 스레드를 다시 첫번째 코루틴에게 양보할 수 있지만 0.5초 라는 비교적 더 긴 시간으로 아직 잠들어있는 상태입니다. 따라서, 스레드가 양보되지 않고 0.1 초 뒤에 다시 launch 내부에서 3번 코드가 실행된 이후에야 runBlocking 으로 스레드가 넘어가는 것을 확인할 수 있습니다. 

    delay 에도 불구하고 양보 안하는 경우

     

    오늘은 코루틴을 배워야하는 이유, 개념 정의, 2가지 코루틴 빌더와 delay 함수에 대해서 알아보았습니다! 

    다음에는 2편으로 다른 메소드나 여러 상황을 다양한 예제를 통해 다뤄보겠습니다. 감사합니다! 

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

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