[Kotlin] 코틀린 코루틴의 정석 (2) - Coroutine 빌더

도톨이·2025년 9월 4일

Kotlin

목록 보기
9/10
post-thumbnail

지난 시간에 이어 코루틴의 정석이라는 책을 읽으며 코루틴을 학습하려고 한다. 책을 읽으며 핵심을 정리하고 그 외 의문들을 확장해 나갈 것이다.
(구매처 : 교보문고 사이트 )

Kotlin 환경 설정

해당 책에서는 인텔리제이로 코틀린을 실습하기 때문에 인텔리제이에 다음처럼 프로젝트를 생성하였다.

  • 이름: coroutines
  • 빌드 시스템: Gradle
  • Gradle 언어: Kotlin
  • JDK: 17 (없다면 설치 필요)

코루틴 라이브러리 추가를 위해 그레이들 파일에 추가한다.
build.gradle.ktsdependencieskotlin 블럭이 다음과 같아야한다.

해당 작업은 kotlinx.coroutines 라이브러리를 추가하는 것이다
(사실 기본적으로 코틀린은 언어 레벨에서 코루틴을 지원하지만 저수준 API만을 지원하기 때문에 실제 개발에 필요한 고수준 API는 코루틴 라이브러리를 통해 제공받을 수 있다)

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2")
}

kotlin {
    jvmToolchain(17) // jvmToolchain 의 jdkVersion 17로 변경
}

코루틴 시작하기

가장 먼저 작성할 수 있는 기본 예제는 아래와 같다.

import kotlinx.coroutines.runBlocking

fun main() = runBlocking<Unit> {
    println("Hello Coroutines")
}

runBlocking은 이름 그대로 스레드를 블로킹(blocking) 하면서 코루틴을 시작한다.
즉, main 함수 안에서 코루틴 스코프를 만들어 내부 블록이 끝날 때까지 메인 스레드를 멈추고 기다린다.
-> 그래서 main 진입점에서 주로 사용

그래서 runBlocking 을 테스트나 예제 코드에서는 유용하지만 실제 안드로이드 UI에서는 잘 쓰이지 않는다. 주로 main 진입점이나 단위 테스트에서 코루틴을 실행하기 위한 진입점으로 사용한다.


코루틴 스코프란?

코루틴은 단독으로 존재하지 않고, 항상 Coroutine Scope(실행 환경) 안에서 실행된다.
스코프는 간단히 말해 코루틴의 생명주기 + 실행 문맥(Context) 을 정의하는 공간이다.

스코프의 역할

  • 생명주기 관리: 스코프가 살아 있는 동안에만 코루틴이 실행된다. 스코프가 취소되면 내부 코루틴도 함께 취소된다.
  • 구조적 동시성 보장: 부모 스코프가 종료될 때, 자식 코루틴도 반드시 완료되거나 취소된다. → 코루틴 누수가 생기지 않는다.
  • 컨텍스트 제공: 어떤 디스패처(Dispatchers.IO, Dispatchers.Main 등)에서 실행할지, 예외 처리를 어떻게 할지 등 실행 환경을 지정한다.

코루틴 빌더 : runBlocking, launch, async

코루틴은 runBlocking, launch, async 같은 코루틴 빌더를 통해 생성된다.

runBlocking 는 현재 스레드를 블로킹하면서 코루틴 스코프를 만든다.
launch 는 반환값이 없는 코루틴을 실행한다. 결과 대신 Job 이라는 객체를 반환한다.
async는 반환값이 있는 코루틴을 실행한다. 결과는 Deferred<T>로 감싸지며 await() 호출 시 값을 얻는다.

아래와 같은 코드가 있다고 하자. 코루틴은 몇 개일까?

import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

fun main() = runBlocking<Unit> {
    println("[${Thread.currentThread().name}] 실행")
    launch {
        println("[${Thread.currentThread().name}] 실행")
    }
    launch {
        println("[${Thread.currentThread().name}] 실행")
    }
}

우선 runBlocking 자체가 하나의 코루틴을 만든다.
그리고 그 내부에서 launch {} 호출 때마다 새로운 코루틴들을 만든다.
따라서 3개의 코루틴이 생성된다.
실행 결과는 다음과 같다.

[main @coroutine#1] 실행
[main @coroutine#2] 실행
[main @coroutine#3] 실행

여기서 기억해야할 것은 Thread.currentThread().name 을 통해 현재 실행 중인 스레드의 이름을 출력할 수 있다. 만약 내 출력처럼 코루틴의 이름(@coroutin#1 등)도 출력하려면 Main.kt의 Edit Configure에서 JVM의 VM option에 -Dkotlinx.coroutines.debug를 추가하면 스레드의 이름 출력 시 코루틴의 이름을 같이 출력할 수 있다.

launch와 async의 차이
launch와 async는 둘 다 새 코루틴을 만들고 (부모 스코프의 자식), 둘 다 구조적 동시성 규칙을 따른다. (부모가 끝나면 자식의 완료/취소 보장됨)
차이는 launch는 결과가 없는 작업(fire-and-forget)이고 async는 결과를 계산해서 반환한다. launchJob이라는 객체를 반환 타입으로 하고, asyncDeferred<T>라는 반환 타입을 가진다. 만약 내가 UI 업데이트/로깅 등 "반환값이 불필요"한 경우에는 launch를 실행하고, 동시에 여러 API 콜 후 결과를 합치고 싶을 때는 async + await()를 사용한다.


📌 요약 정리

  • runBlocking
    main 함수 진입점이나 테스트에서 코루틴을 실행하기 위해 사용. 내부 블록이 끝날 때까지 스레드를 블로킹한다.

  • Coroutine Scope

    • 코루틴은 항상 스코프 안에서 실행된다.
    • 스코프는 생명주기를 관리하고, 구조적 동시성을 보장하며, 실행 컨텍스트(디스패처 등)를 제공한다.
    • 부모 스코프가 종료되면 자식 코루틴도 반드시 완료되거나 취소된다.
  • Coroutine Builder

    • runBlocking: 현재 스레드를 블로킹하면서 스코프 생성.
    • launch: 결과 없는 작업 실행, Job 반환. fire-and-forget 용도.
    • async: 값을 반환하는 작업 실행, Deferred<T> 반환. await()으로 결과 획득.
  • launch vs async

    • launch: UI 업데이트, 로그 기록, 캐시 저장 등 → 반환값 필요 없는 작업.
    • async: 여러 API 결과를 합치거나 병렬 연산을 수행할 때 사용.

📝 확인 문제

  1. runBlocking 은 어떤 상황에서 주로 사용되는가?

  2. Coroutine Scope의 주요 역할 3가지를 설명하라.

  3. 아래 코드에서 코루틴은 총 몇 개 생성되는가?

    fun main() = runBlocking {
        launch { println("A") }
        launch { println("B") }
    }
  4. launchasync 의 차이점을 반환값과 예외 처리 관점에서 설명하라.

  5. UI 이벤트 트래킹, 캐시 저장, 여러 API 동시 호출 → 각각 launch/async 중 어떤 것을 쓰는 게 적합한가?


✅ 모범 답안

  1. runBlocking 은 어떤 상황에서 주로 사용되는가?
    → main 함수 진입점이나 테스트 코드에서 코루틴을 실행할 때 사용한다. 실제 안드로이드 UI에서는 스레드를 블로킹하므로 사용하지 않는다.

  2. Coroutine Scope의 주요 역할 3가지

    • 생명주기 관리: 스코프가 살아 있는 동안만 코루틴이 실행된다.
    • 구조적 동시성 보장: 부모 스코프 종료 시 자식 코루틴도 완료되거나 취소된다.
    • 컨텍스트 제공: 실행할 디스패처, 예외 처리 방식 등 환경을 정의한다.
  3. 코루틴 개수
    runBlocking 1개 + launch 2개 = 총 3개의 코루틴이 생성된다.

  4. launch vs async 차이 (반환값 & 예외 처리)

    • launch: 반환값 없음, Job 반환. 예외 발생 시 즉시 부모로 전파.
    • async: 결과를 반환(Deferred<T>), await() 호출 시 예외가 재던져진다.
  5. 상황별 적합한 선택

    • UI 이벤트 트래킹 → launch
    • 캐시 저장 → launch
    • 여러 API 동시 호출 후 합치기 → async
profile
Kotlin, Flutter, AI | Computer Science

0개의 댓글