Search

Coroutines basics

Coroutine

코루틴은 일시 중단할 수 있는 계산 인스턴스이다.
개념적으로는 스레드와 유사하게 코드 블록을 실행하며 나머지 코드와 병렬로 동작한다.
하지만 코루틴은 특정 스레드에 종속되지 않는다.
한 스레드에서 실행을 중단할 수 있으며 다른 스레드에서 다시 실행을 재개할 수 있다.
코루틴은 가벼운 스레드로 생각할 수 있지만 실제 사요 시 스레드와는 몇 가지 중요한 차이점이 있으며 이로 인해 스레드와는 상당히 다르게 동작한다.

예제 코드

fun main() = runBlocking { // this: CoroutineScope launch { // 새로운 코루틴을 시작하고 계속 실행 delay(1000L) // 1초 동안 비차단 대기 (기본 시간 단위는 밀리초) println("World!") // 대기 후 출력 } println("Hello") // 이전 코루틴이 대기 중일 때 메인 코루틴은 계속 실행 } // 출력: // Hello // World!
Kotlin
복사
launch
코루틴 빌더이다.
이것은 나머지 코드와 병렬로 새로운 코루틴을 실행하며 코드는 독립적으로 계속 실행된다.
delay
특별한 일시 중단 함수이다.
이것은 코루틴을 특정 시간 동안 중단시키며 해당 코루틴이 실행되는 스레드를 차단하지 않으므로 다른 코루틴이 해당 스레드를 사용하여 실행될 수 있다.
runBlocking
코루틴 빌더이다.
일반 함수인 main() 과 코루틴 코드를 연결해준다.
Unresolved reference: launch
Kotlin
복사
launch는 CoroutineScope에서만 선언되었기 때문에 만약 runBlocking 을 제거하거나 잊어버리면 launch 호출 시 오류가 발생한다.
runBlocking은 해당 스레드가 코루틴이 모두 완료될 때까지 차단된다는 뜻이다.
이 방식은 응용 프로그램의 가장 상위 레벨에서 자주 사용되며 실제 코드에서는 거의 사용되지 않는다.
스레드는 리소스가 많이 드는 자원이기 때문에 차단하는 것은 비효율적이다.

구조적 동시성

코루틴은 구조적 동시성이라는 원칙을 따른다.
즉, 새로운 코루틴은 특정 CoroutineScope 내에서만 시작할 수 있으며 이 스코프는 코루틴의 생명 주기를 한정한다.
실제 어플리케이션에서는 많은 코루틴을 실행하게 된다.
구조적 동시성은 이러한 코루틴이 누락되거나 메모리 누수가 발생하지 않도록 보장한다.
외부 스코프는 자식 코루틴이 모두 완료될 때까지 완료되지 않으며 코드의 모든 오류가 적절하게 보고되고 손실되지 않도록 보장한다.

함수 추출 리팩토링

fun main() = runBlocking { // this: CoroutineScope launch { doWorld() } println("Hello") } // 첫 번째 일시 중단 함수 suspend fun doWorld() { delay(1000L) println("World!") }
Kotlin
복사
launch { … } 블록 내부의 코드를 별도의 함수로 추출해보자.
이 코드를 리팩토링하면 suspend 수정자가 있는 새로운 일시 중단 함수가 만들어진다.
일시 중단 함수는 일반 함수처럼 코루틴 내부에서 사용할 수 있으며 추가로 다른 일시 중단 함수(delay 같은)를 사용하여 코루틴 실행을 일시 중단할 수 있다.

스코프 빌더

fun main() = runBlocking { doWorld() } suspend fun doWorld() = coroutineScope { // this: CoroutineScope launch { delay(1000L) println("World!") } println("Hello") }
Kotlin
복사
코루틴 스코프는 코루틴 빌더에 의해 제공되지만 coroutineScope 빌더를 사용하여 직접 선언할 수도 있다.
이는 코루틴 스코프를 생성하며 모든 자식 코루틴이 완료될 때까지 완료되지 않는다.
runBlocking과 coroutineScope 빌더는 둘 다 본문과 자식 코루틴이 완료될 때까지 기다린다는 점에서 유사하다.
주요 차이점은 runBlocking은 현재 스레드를 차단하여 기다리는 반면 coroutineScope는 일시 중단하여 스레드를 다른 용도로 사용할 수 있도록 한다.
이러한 차이로 인해 runBlocking은 일반 함수이고 coroutineScope는 일시 중단 함수이다.

스코프 빌더와 동시성

coroutineScope 빌더는 일시 중단 함수 안에서 여러 개의 동시 작업을 수행하는데 사용할 수 있다.
doWorld라는 일시 중단 함수 안에서 두 개의 동시 코루틴을 실행할 수 있다.
// doWorld가 순차적으로 실행되고, 그 후에 "Done"이 출력됨 fun main() = runBlocking { doWorld() println("Done") } // 두 섹션이 동시 실행됨 suspend fun doWorld() = coroutineScope { // this: CoroutineScope launch { delay(2000L) println("World 2") } launch { delay(1000L) println("World 1") } println("Hello") } // 출력: // Hello // World 1 // World 2 // Done
Kotlin
복사
두 개의 launch { … } 블록 안의 코드가 동시에 실행되며 시작 후 1초가 지나면 “World 1” 이 먼저 출력되고 2초가 지나면 “World 2” 가 출력된다.
doWorld 내부의 coroutineScope 는 두 개의 코루틴이 모두 완료된 후에야 완료되므로 doWorld 가 변환되고 나서 “Done” 문자열이 출력된다.

명시적인 Job

val job = launch { // 새로운 코루틴을 시작하고 Job을 참조로 유지 delay(1000L) println("World!") } println("Hello") job.join() // 자식 코루틴이 완료될 때까지 대기 println("Done") // 출력: // Hello // World! // Done
Kotlin
복사
launch 코루틴 빌더는 Job 객체를 반환하며 이는 시작된 코루틴에 대한 핸들로 사용될 수 있고 이를 통해 명시적으로 코루틴의 완료를 기다릴 수 있다.

코루틴은 경량이다.

코루틴은 JVM 스레드보다 리소스를 덜 사용한다.
스레드를 사용할 때 JVM 메모리를 초과하여 프로그램이 종료될 수 있는 코드도 코루틴을 사용하면 리소스 한계를 초과하지 않는다.
import kotlinx.coroutines.* fun main() = runBlocking { repeat(50_000) { // 많은 코루틴 실행 launch { delay(5000L) print(".") } } }
Kotlin
복사
만약 이 프로그램을 스레드를 사용하여 작성한다면(runBlocking을 제거하고 launch를 thread로, delay를 Thread.sleep으로 변경한다면) 많은 메모리를 소비하게 된다.
운영체제, JDK 버전 그리고 설정에 따라 메모리 부족 오류가 발생하거나 너무 많은 스레드를 동시에 실행하지 않도록 스레드가 천천히 시작될 수 있다.