개발자의 생활

coroutine(코루틴) 본문

kotlin

coroutine(코루틴)

Developer성현 2026. 2. 11. 02:27

코루틴 은 코틀린에서 비동기 처리를 쉽고 가볍게 할 수 있도록 제공하는 라이브러리입니다.

 

비동기 란

순차적으로 실행되는 것을 동기 라고 합니다. 하지만 서비스를 개발하다 보면 네트워크를 통해 데이터를 주고받는 작업이 거의 필수적이지만 이런 작업은 시간이 얼마나 걸릴지 모른다는 것입니다. 그래서 이런 작업은 호출을 한 뒤에 언제 응답이 올지 모르기 때문에 미뤄놓고 다음 작업을 먼저 수행하고 응답이 왔을 때 이어서 수행하는 것을 비동기 작업이라고 합니다.

 

그럼 이런 작업이 어떻게 가능할까요? 

OS는 여러 개의 스레드를 가지고 있습니다. 스레드는 마치 여러 프로그램을 동시에 실행시킬 수 있는 것처럼 도와주는 역할을 합니다. 하지만 스레드 또한 개수가 제한적이고 사용할 때 비용이 크기 때문에 남용해서는 안됩니다. 비용이 크다는 건 하나의 스레드가 하나의 프로그램을 도맡아서 처리하는 것이 아니고 여러 프로그램을 하나의 스레드가 번갈아 가면서 작업을 수행하는데 이 과정에서 다른 프로그램들의 수행하전 중간 데이터를 메모리에 저장을 해야 하기 때문입니다.

 

코루틴이 처리하는 방식

스레드를 사용하기 위해서는 비용이 크다고 했습니다. 그래서 코루틴은 여러 스레드를 사용하는 방법이 아닌 중단시키는 방식으로 작업을 처리합니다.

중단시킨다는 말은 스레드가 네트워크 처리와 같은 오래 걸리고 언제 끝날 지 모르는 작업들은 계속 점유하는 게 아닌 잠시 내려놓고 해당 스레드를 다른 작업을 실행할 수 있도록 넘겨준 뒤 해당 작업이 완료되면 다시 올라와서 스레드가 처리해 주는 방식입니다.

 

* 코루틴의 오해 *

코루틴은 2가지 이상의 작업을 동시에 처리하는 게 아닙니다. 정확히 말하면 잠시 정지시키는 것입니다. 다만 스레드를 점유한 상태에서 정지하는 것이 아니라 해당 스레드가 다른 작업을 계속 수행할 수 있도록 풀어준 뒤 완료되면 다시 스레드가 이어서 작업을 처리해 주는 것입니다.

 

 코루틴 사용하기

import kotlinx.coroutines.*

코루틴을 사용하기 위해서는 위 라이브러리를 가져와야 합니다.

 

fun main(){
    runBlocking {
        println("start")
        delay(1000)
        println("stop")
    }
}

위 코드를 실행해 보면 start 가 출력된 후 약 1초 뒤에 stop 가 출력이 됩니다.

그런데 이 코드들이 runBlocking라는 람다함수 내부에 있습니다. runBlocking 은 내부 로직이 모두 수행될 때까지 메인 스레드를 붙잡아 두는 역할을 합니다.

만약 이 함수가 없다면 delay로 시간을 지연시킬 수 없습니다. 이 delay 함수가 자로 정지함수입니다.

 

한번 delay 함수의 구조를 간단히 보겠습니다

public suspend fun delay(timeMillis: Long)

 

이 함수에는 suspend라는 키워드가 있습니다. 이 키워드는 해당 함수가 정지함수라는 의미입니다.

그럼 저의도 정지함수를 만들어 보겠습니다.

suspend fun networkGetText(): String{
    delay(2000)
    return "데이터1"
}

 

이렇게 네트워크에서 텍스트를 가져오는 모의 함수를 만들었습니다.

* 여기서 중요한 규칙이 있습니다. 정지함수는 정지함수 내부 또는 runBlocking와 같은 루틴을 대기할 수 있는 특수 함수 내부에서만 사용할 수 있습니다.

fun main(){
    runBlocking {
        println("start")
        println(networkGetText())
        println("stop")
    }
}
suspend fun networkGetText(): String{
    delay(2000)
    return "데이터1"
}

이 코드도 동일하게 runBlocking에서

역시 start 가 출력된 후에 약 2초 뒤 데이터 1과 stop 가 출력이 됩니다.

 

하지만 보시다시피 이 코드는 동기적으로 동작을 하는 최악의 코드입니다.

 

그럼 이번에는 정말 코루틴을 사용해 보겠습니다.

방법 1 (launch)

fun main(){
    runBlocking {
        println("start")
        launch {
            println(networkGetText())
        }
        println("end")
    }
}

 

launch는 코루틴을 생성하는 람다함수입니다. 이 함수 내부에 정지함수를 넣으면 코루틴에 의해 정지함수는 특수한 메모리 공간에 들어가서 작업이 끝날 때까지 정지하고 해당 스레드는 계속 다음 작업을 수행하게 됩니다. 그리고 정지함수의 작업이 끝나면 다시 스레드에 의해 이어서 수행하게 됩니다.

 

그래서 이 코드를 실행해 보면 start 가 출력된 후 바로 end 가 출력이 되고 약 2초 뒤 데이터 1 이 출력됩니다.

 

방법 2 (Async)

Async는 launch와

직접 사용해 보겠습니다.

fun main(){
    runBlocking {
        println("start")
        val deferred = async {
            networkGetText()
        }
        println(deferred.await())
        println("end")
    }
}

async는 반환할 때  Deferred 객체 타입으로 반환하게 됩니다. 그리고 await() 함수를 통해 반환값을 가져올 수 있습니다.

여기서 중요한 건 Deferred 타입의 객체를 정지함수가 실행이 완료될 때 반환하는 것이 아니라 async  함수가 호출될 때 바로 반환한다는 것입니다.

그리고 await()를 통해 결과가 출력됐는지를 감시하고 있다가 async 내부의 값이 반환되면 await()를 통해 가져오게 됩니다.

그런데 이 코드를 실행해 보시면 end 문자열이 await()를 통해 반한 된 후 실행되는 것을 보실 수 있습니다.

즉 await() 도 정지함수라는 것입니다.

 

이렇게 코루틴을 사용해서 정지함수를 호출해도 스레드를 효율적으로 계속 사용할 수 있는 방법을 알았습니다.

 

그럼 한 가지 간단한 실험을 해보겠습니다.

만약 코루틴 내부에서 일반 함수를 호출했는데 이 함수가 처리되는 시간이 1초라면 어떻게 될까요?

한번 테스트해 보겠습니다

 

fun main(){
    runBlocking {
        println("start")
        launch {
            Thread.sleep(1000)
        }
        println("end")
    }
}

 

이 코드는 코루틴이 생성되고 내부에서 스레드를 강제로 1초 동안 멈추게 합니다.

중요한 건 sleep는 중지함수가 아니라는 것입니다. 즉 메인 스레드는 1초 동안 점추게 되어 아무런 작업도 못하게 되는 것입니다.

 

하지만 코드를 실행해 보면 이상하게도 start 가 출력되고 바로 end 가 출력된 후에 코루틴이 내부 로직이 수행된다는 것입니다.

이렇게 동작하는 이유는 부모의 코드가 아직 끝나지 않았기 때문입니다. 조금 바꿔 말하면 부모의 코드가 메인스레드를 계속 점유하고 있었기 때문입니다. launch는 부모에게서 생성된 코루틴 이기 때문에 부모(상위)에서 실행되는 스레드의 점유가 끝나야 자식 코루틴이 사용할 수 있게 됩니다.

 

즉 지금까지 launch는 부모의 코드가 스레드를 잠시 내려놓았을 때 실행을 하게 된 것입니다. 그럼 코드를 조금 바꿔보겠습니다.

fun main(){
    runBlocking {
        println("start")
        launch {
            Thread.sleep(1000)
        }
        delay(1)
        println("end")
    }
}

 

이렇게 하면 부모코드가 실행하다 delay 정지함수를 만나 1ms 약 0.001초 동안 정지를 하고 있으면 스레드는 이 작은 틈에도 일을 하기 위해 이전에 코루틴이 생성된 부분을 수행하러 가는 것입니다.

 

Dispatchers로 스레드 분리

만약 이런 상황에서도 동시에 실행시키고 싶다면 스레드를 분리할 수 있습니다.

지금까지는 프로그램이 실행될 때 점유한 메인 스레드만 가지고 작업을 하였습니다. 하지만 코루틴을 수행할 때 다른 스레드를 사용할 수도 있습니다.

스레드의 종류는 대표적으로 3가지가 있습니다.

1. Main: 프로그램이 실행될 때 기본으로 제공된 스레드입니다.

2. IO: 네트워크 또는 파일 입출력 수행에 특화된 스레드입니다.

3. Default: 이 스레드는 많은 양의 연산에 특화된 스레드입니다. 아무것도 지정하지 않은 상태라면 해당 스레드를 사용합니다.

 

*중요 저의는 지금까지 launch 나 async를 사용할 때 스레드를 지정하지 않았습니다. 하지만 실제로 사용된 스레드는 Default 가 아닌 main입니다. 그 이유는 runBlocking 내부에서 실행하고 있기 때문입니다.

launch 나 async는 상위에서 이미 제공되는 스레드가 있으면 그것을 이어받아 사용하게 됩니다.

(확인방법)

println(Thread.currentThread().name)

스레드가 동작하는 부분에 이 코드를 실행시키면 스레드 이름이 나옵니다.

 

fun main(){
    runBlocking {
        println("start")
        launch(Dispatchers.IO) {
            Thread.sleep(1000)
            println(networkGetText())
        }
        delay(1)
        // yield() 추천
        println("end")
    }
}

이렇게 하면 launch 부분에서 코루틴이 생성이 되었을 때 이 코루틴을 실행하는 스레드는 IO 가 되는 것입니다.

그리고 main 스레드는 1ms를 쉬고 바로 작업을 수행할 수 있게 되는 것입니다.

 

여기서 delay(1)는 뭔가 꼼수처럼 보이기도 하고 컴퓨터 입장에서는 1ms 도 느린 시간이기 때문에 상당히 안 좋을 수 있습니다. 그래서 이런 경우에는 yield()를 사용하는 것이 더 올바른 코드라고 볼 수 있습니다.

yield()는 제어권을 자식이라고 해도 자식이 먼저 호출을 했다면 권한을 넘겨주게 됩니다.

 

스레드를 분리하는 방법 중에서도 withContext()를 사용할 수 있습니다.

withContext()의 특징은 코루틴을 생성하지 않고 단순히 스레드만 변경하기 위한 용도입니다. 또한 정지함수입니다.

 

fun main(){
    runBlocking {
        println("start")
        withContext(Dispatchers.IO) {
            Thread.sleep(1000)
            println(networkGetText())
        }
        println("end")
    }
}

 

이 코드를 실행해 보면 단순히 동기적으로 실행이 됩니다. launch , async 와는 엄연히 다른 역할을 하는 정지함수입니다.

 

코루틴 강제종료

코루틴을 사용해서 네트워크에 요청을 하는 경우 응답이 오면 자동으로 코루틴이 종료가 됩니다. 하지만 응답이 오기 전에 사용자가 회면을 나간다거나 취소버튼을 누르면 코루틴은 기다릴 필요가 없게 되니 직접 종료를 해야 하는 경우가 있습니다. 혹은 요청을 하고 10초가 지났는데도 응답이 없으면 스스로 종료를 하는 경우도 있습니다.

 

이번에는 실행 중인 코루틴을 강제로 종료하는 방법을 알아보겠습니다.

launch와 async 함수는 호출이 되면 객체를 반환하게 됩니다.

launch는

async는 Deferred 타입의 객체를 반환합니다. ( Deferred는 Job를 상속하고 있습니다. )

Job는

cancel()를 사용하면 실행 중인 코루틴을 종료할 수 있습니다.

 

fun main(){
    runBlocking {
        val job: Job = launch {
            for (i in 0..< 5){
                println(i)
                delay(1000)
            }
        }
        launch {
            delay(3000)
            job.cancel()
        }
    }
}

 

이 코드를 실행하면 3 까지만 나오고 종료되는 것을 보실 수 있습니다.

 

지금까지 코루틴을 알아보았습니다.

끝까지 봐주셔서 감사합니다.

'kotlin' 카테고리의 다른 글

[ kotlin ] 자료구조_Stack, Queue, Deque  (0) 2025.10.23
sealed class  (0) 2025.09.16
Enum Class  (0) 2025.09.14
data class  (2) 2025.09.12
중첩 클래스, 내부클래스  (0) 2025.09.08