IntelliJ IDEA로 Kotlin Flow 디버깅하기

IntelliJ IDEA로 Kotlin Flow 디버깅하기 (Debug Kotlin Flow using IntelliJ IDEA – tutorial)

Kotlin Flow를 만들고 IntelliJ IDEA로 디버깅하는 방법을 알아보는 튜토리얼이에요.

이 튜토리얼은 coroutinesflow 개념을 미리 알고 있다는 전제로 진행해요.

출처: Debug Kotlin Flow using IntelliJ IDEA – tutorial

본문

Kotlin flow 만들기 (Create a Kotlin flow)

느린 emitter와 느린 collector를 가진 Kotlin flow를 만들어 볼게요.

  • IntelliJ IDEA에서 Kotlin 프로젝트를 열어요. 프로젝트가 없다면 하나 만들면 돼요.
  • Gradle 프로젝트에서 kotlinx.coroutines 라이브러리를 사용하려면 build.gradle(.kts)에 다음 의존성을 추가해요.
dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}
dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0'
}

다른 빌드 시스템은 kotlinx.coroutineskotlinx.coroutines README에 나온 안내를 참고하면 돼요.

  • src/main/kotlin에 있는 Main.kt 파일을 열어요. src 디렉터리에는 Kotlin 소스 파일과 리소스가 들어 있어요. Main.kt 파일에는 Hello World!를 출력하는 샘플 코드가 담겨 있어요.
  • 세 개의 숫자를 흘려보내는(emit) flow를 반환하는 simple() 함수를 만들어요.
  • CPU를 많이 쓰는 차단 코드를 흉내 내기 위해 delay()delay() 함수를 사용해요. 이 함수는 스레드를 블록하지 않고 코루틴을 100ms 동안 중단시켜요.
  • for 루프에서 emit()emit() 함수로 값을 만들어 내요.
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import kotlin.system.*

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        delay(100)
        emit(i)
    }
}
  • main() 함수의 코드를 다음과 같이 바꿔요.
  • 코루틴을 감싸려면 runBlocking()runBlocking() 블록을 사용해요.
  • collect()collect() 함수로 만들어 낸 값을 수집해요.
  • CPU를 많이 쓰는 코드를 흉내 내기 위해 delay()delay() 함수를 사용해요. 이 함수는 스레드를 블록하지 않고 코루틴을 300ms 동안 중단시켜요.
  • println()println() 함수로 flow에서 수집한 값을 출력해요.
fun main() = runBlocking {
    simple()
        .collect { value ->
            delay(300)
            println(value)
        }
}
  • Build Project를 클릭해서 코드를 빌드해요.

코루틴 디버깅하기 (Debug the coroutine)

  • emit() 함수가 호출되는 줄에 중단점(breakpoint)을 걸어요.
  • 화면 위쪽 실행 구성 옆의 Debug를 클릭해서 코드를 디버그 모드로 실행해요. 그러면 Debug 도구 창이 나타나요.
    • Frames 탭에는 호출 스택이 들어 있어요.
    • Variables 탭에는 현재 컨텍스트의 변수들이 들어 있어요. 여기서 flow가 첫 번째 값을 만들고 있음을 알 수 있어요.
    • Coroutines 탭에는 실행 중이거나 중단된 코루틴에 대한 정보가 들어 있어요.
  • Debug 도구 창에서 Resume Program을 클릭해서 디버거 세션을 재개해요. 프로그램은 같은 중단점에서 다시 멈춰요. 이제 flow가 두 번째 값을 만들어 내요.

최적화되어 없어진 변수 (Optimized-out variables)

suspend 함수를 사용하면 디버거에서 변수 이름 옆에 "was optimized out"이라는 텍스트가 보일 수 있어요.

이 텍스트는 변수의 수명이 줄어들어 변수가 더 이상 존재하지 않는다는 뜻이에요. 최적화된 변수들은 값을 볼 수 없어서 디버깅하기 어렵죠. 이 동작은 -Xdebug 컴파일러 옵션으로 끌 수 있어요.

WARNING: 이 플래그는 절대 프로덕션에서 쓰지 마세요. -Xdebug메모리 누수를 일으킬 수 있거든요.

동시에 실행되는 코루틴 추가하기 (Add a concurrently running coroutine)

  • src/main/kotlin에 있는 Main.kt 파일을 열어요.
  • emitter와 collector가 동시에 실행되도록 코드를 보강해요.
  • emitter와 collector를 동시에 실행하려면 buffer()buffer() 함수 호출을 추가해요. buffer()는 만들어 낸 값을 저장하고 flow collector를 별도의 코루틴에서 실행시켜요.
fun main() = runBlocking<Unit> {
    simple()
        .buffer()
        .collect { value ->
            delay(300)
            println(value)
        }
}
  • Build Project를 클릭해서 코드를 빌드해요.

두 개의 코루틴으로 Kotlin flow 디버깅하기 (Debug a Kotlin flow with two coroutines)

  • println(value)에 새 중단점을 걸어요.
  • 화면 위쪽 실행 구성 옆의 Debug를 클릭해서 코드를 디버그 모드로 실행해요. 그러면 Debug 도구 창이 나타나요. Coroutines 탭에서 두 개의 코루틴이 동시에 실행되고 있음을 확인할 수 있어요. buffer() 함수 덕분에 flow collector와 emitter가 별도의 코루틴에서 실행되거든요. buffer() 함수는 flow에서 만들어 낸 값을 버퍼링해요. emitter 코루틴은 RUNNING 상태이고, collector 코루틴은 SUSPENDED 상태예요.
  • Debug 도구 창에서 Resume Program을 클릭해서 디버거 세션을 재개해요. 이제 collector 코루틴이 RUNNING 상태가 되고, emitter 코루틴이 SUSPENDED 상태가 돼요. 각 코루틴을 더 깊이 들여다보며 코드를 디버깅할 수 있어요.