2020년 7월 29일
우리는 계속해서 1.4 릴리스에서의 예정된 변경 사항을 강조하고 있습니다. 이 블로그 포스트에서는 코루틴과 관련된 중요한 두 가지 기능을 설명하려고 합니다.
- 코루틴을 편리하게 디버깅할 수 있는 새로운 기능
- 깊은 재귀 함수를 정의할 수 있는 능력
이러한 변경 사항은 이미 1.4.0-RC 릴리스에서 사용할 수 있습니다!
자세히 살펴보겠습니다.
코루틴 디버깅
코루틴은 비동기 프로그래밍에 매우 효과적입니다(하지만 그것만을 위한 것은 아닙니다) 그리고 이미 많은 사람들이 사용하거나 사용하기 시작하고 있습니다. 그러나 코루틴으로 코드를 작성할 때 디버깅하려고 하면 실제로 귀찮은 작업 일 수 있습니다. 코루틴은 스레드 간을 이동합니다. 특정 코루틴이 무엇을 하는지 이해하기 어려울 수 있으며 해당 코루틴의 컨텍스트를 확인하는 것도 어려울 수 있습니다. 또한 경우에 따라 중단점에서 단계를 추적하는 것이 단순히 작동하지 않을 수 있습니다. 결과적으로 코루틴으로 된 코드를 디버깅하기 위해서는 로깅이나 정신적 노력을 의존해야 할 수 있습니다. 이 문제를 해결하기 위해 Kotlin 플러그인에 디버깅 코루틴을 훨씬 더 편리하게 만드는 새로운 기능을 도입하고 있습니다.
디버그 도구 창에는 이제 새로운 "Coroutines" 탭이 있습니다. 기본적으로 표시되며 표시/숨기기를 전환할 수 있습니다.
이 탭에서는 현재 실행 중인 코루틴과 일시 중단된 코루틴에 대한 정보를 찾을 수 있습니다. 코루틴은 해당하는 디스패처로 그룹화됩니다. 사용자 정의 이름으로 코루틴을 시작한 경우, 도구 창에서 해당 이름으로 찾을 수 있습니다. 다음 예에서는 메인 코루틴이 실행 중임을 볼 수 있으며(우리는 그 안에서 중단점에 멈춰 있습니다), 다른 네 개의 코루틴은 일시 중단되어 있음을 볼 수 있습니다.
import kotlinx.coroutines.*
fun main() = runBlocking {
repeat(4) {
launch(Dispatchers.Default + CoroutineName("Default-${'a' + it}")) {
delay(100)
val name = coroutineContext[CoroutineName.Key]?.name
println("I'm '$name' coroutine")
}
}
delay(50)
// breakpoint
println("I'm the main coroutine")
}
새로운 기능을 사용하면 각 코루틴의 상태를 확인하고 로컬 및 캡처된 변수의 값을 볼 수 있습니다. 이것은 일시 중단된 코루틴에 대해서도 작동합니다!
다음 예에서는 일시 중단된 코루틴의 로컬 변수 값을 확인합니다.
import kotlinx.coroutines.*
fun main() = runBlocking<Unit> {
launch {
val a = 3
delay(300)
println(a)
}
launch {
val b = 2
delay(200)
println(b)
}
launch {
val c = 1
delay(100)
// breakpoint here:
println(c)
}
}
일시 중단된 코루틴의 상태를 확인하려면 중단점에 "Suspend: All" 옵션을 선택해야만 디버거가 중단합니다.
이제 코루틴 생성 스택과 코루틴 내부의 호출 스택을 완전히 볼 수 있습니다.
'코루틴 덤프 가져오기' 옵션을 사용하여 각 코루틴의 상태와 스택을 포함한 전체 보고서를 얻을 수 있습니다.
현재 코루틴 덤프는 아직 간단합니다. 그러나 향후 버전에서 더 읽기 쉽고 유용하게 만들 계획입니다.
코루틴 내부에서 중단점에서 디버거를 중단하려면 해당 중단점에 "중단: 모두" 옵션을 선택해야 합니다.
이 새로운 코루틴 디버깅 기능을 사용하려면 kotlinx.coroutines의 최신 버전인 1.3.8-1.4.0-rc 및 Kotlin 플러그인의 최신 버전(e.g. 1.4.0-rc-release-IJ2020.1-2)을 사용해야 합니다.
이 기능은 Kotlin/JVM에 대해서만 사용 가능합니다. 문제가 발생하는 경우(자세한 정보를 공유하지 않도록 주의하세요!), 환경 설정에서 빌드, 실행, 배포 | 디버거 | 데이터 보기 | Kotlin에서 "코루틴 에이전트 비활성화"를 선택하여 이 기능을 끌 수 있습니다. 현재 이 기능은 코루틴 디버깅을 위해 실험적 상태로 배포되었으며 여러분의 피드백을 기다리고 있습니다!
코루틴을 사용하여 깊은 재귀 함수 정의
Kotlin 1.4에서는 표준 라이브러리 지원을 사용하여 호출 깊이가 100,000을 초과하는 재귀 함수를 정의하고 호출할 수 있습니다!
먼저 호출 깊이가 높아질 때 StackOverflowError를 결과로 반환하는 일반적인 재귀 함수를 살펴보겠습니다. 그런 다음 문제를 해결하고 Kotlin 표준 라이브러리를 사용하여 함수를 다시 작성하는 방법을 설명하겠습니다.
우리는 각 Tree 노드가 왼쪽과 오른쪽 자식에 대한 참조를 가진 간단한 이진 트리를 사용할 것입니다:
class Tree(val left: Tree?, val right: Tree?)
트리의 깊이는 루트에서 자식 노드까지 가장 긴 경로의 길이입니다. 다음과 같은 재귀 함수를 사용하여 계산할 수 있습니다:
fun depth(t: Tree?): Int =
if (t == null) 0 else maxOf(
depth(t.left),
depth(t.right)
) + 1
트리의 깊이는 왼쪽 및 오른쪽 자식의 깊이 중 가장 큰 값에 1을 더한 것입니다. 트리가 비어 있으면 0입니다.
이 함수는 재귀 깊이가 작을 때 잘 작동합니다:
val tree = Tree(Tree(Tree(null, null), null), null)
println(depth(tree)) // 3
그러나 실제로는 그리 흔하지 않지만 깊이가 100,000을 초과하는 트리를 생성하면 StackOverflowError가 발생합니다:
fun main() {
val n = 100_000
val deepTree = generateSequence(Tree(null, null)) { prev ->
Tree(prev, null)
}.take(n).last()
println(depth(deepTree))
}
문제는 호출 스택이 너무 커진다는 것입니다. 이 문제를 해결하려면 최대 스택 크기를 늘리는 VM 옵션을 사용할 수 있습니다. 그러나 이것은 특정 사용 사례에는 작동할 수 있지만 일반적인 경우에는 실용적인 해결책이 아닙니다.
대신 코드를 다시 작성하고 중간 호출 결과를 스택이 아닌 힙에 수동으로 저장하여 문제를 해결할 수 있습니다. 이 해결책은 대부분의 경우에 작동하며 다른 언어에서 흔한 방법입니다. 그러나 결과 코드는 복잡하고 initial 함수의 아름다움과 간결함이 사라집니다. 여기에서 예제를 찾을 수 있습니다.
Kotlin은 이 문제를 해결하기 위해 코루틴 메커니즘을 기반으로 하는 깔끔한 방법을 제공합니다.
Kotlin 라이브러리에는 재귀 호출을 중단 메커니즘을 사용하여 모델링하는 DeepRecursiveFunction의 정의가 포함되어 있습니다.
class Tree(val left: Tree?, val right: Tree?)
@OptIn(ExperimentalStdlibApi::class)
val depthFunction = DeepRecursiveFunction<Tree?, Int> { t ->
if (t == null) 0 else maxOf(
callRecursive(t.left),
callRecursive(t.right)
) + 1
}
@OptIn(ExperimentalStdlibApi::class)
fun depth(t: Tree) = depthFunction(t)
fun main() {
val n = 100_000
val deepTree = generateSequence(Tree(null, null)) { prev ->
Tree(prev, null)
}.take(n).last()
println(depth(deepTree)) // 100000
}
두 버전, 초기 버전과 DeepRecursiveFunction을 사용한 버전을 비교하여 로직이 동일한지 확인할 수 있습니다. 새로운 함수는 이제 DeepRecursiveFunction 유형의 변수가 되며 depthFunction(t)와 같은 'invoke' 규칙을 사용하여 호출할 수 있습니다. 함수 본문은 이제 DeepRecursiveFunction의 람다 인수의 본문이 되고 재귀 호출은 callRecursive로 대체됩니다. 이 변경 사항은 직관적이고 쉽게 적용할 수 있습니다. 새로운 depth 함수는 내부적으로 코루틴을 사용하며 suspend 함수 자체는 아닙니다.
DeepRecursiveFunction가 어떻게 구현되었는지 이해하는 것은 흥미롭지만 사용하고 그 이점을 얻기 위해 이해할 필요는 없습니다. 구현 세부 정보는 이 블로그 게시물에서 설명되어 있습니다.
DeepRecursiveFunction은 kotlinx.coroutines 라이브러리가 아니라 Kotlin 표준 라이브러리의 일부입니다. 현재 이 API는 실험적 상태이므로 여러분의 피드백을 기다리고 있습니다!
시도하는 법
당신은 Kotlin을 온라인에서 사용해 볼 수 있습니다. play.kotl.in에서 시도할 수 있습니다.
IntelliJ IDEA에서 Kotlin 플러그인을 1.4.0-RC 버전으로 업데이트할 수 있습니다. 이를 어떻게 하는지 알아보세요.
미리 보기 버전을 설치하기 전에 만들어진 기존 프로젝트에서 작업하려면 Gradle 또는 Maven에서 미리 보기 버전을 설정해야 합니다. 이전의 미리 보기 버전과 달리 Kotlin 1.4.0-RC는 Maven Central에서 직접 사용할 수 있습니다. 이것은 빌드 파일에 kotlin-eap 저장소를 수동으로 추가할 필요가 없다는 것을 의미합니다.
GitHub 릴리스 페이지에서 명령줄 컴파일러를 다운로드할 수도 있습니다.
피드백을 공유해 주세요.
버그를 찾아서 이슈 트래커에 보고해 주신다면 매우 감사하겠습니다. 중요한 문제들을 최종 릴리스 전에 해결하려고 노력할 것이며, 이는 여러분의 문제가 해결되기 위해 다음 Kotlin 릴리스를 기다릴 필요가 없다는 것을 의미합니다.
또한 Kotlin Slack의 #eap 채널에 참여할 것을 환영합니다 (여기에서 초대를 받으세요). 이 채널에서 질문을 하거나 토론에 참여하고 새로운 미리 보기 빌드에 대한 알림을 받을 수 있습니다.
Kotlin을 사용해 봅시다!
원문
https://blog.jetbrains.com/kotlin/2020/07/kotlin-1-4-rc-debugging-coroutines/
댓글