Kotlin에서 코루틴은 어떻게 구현될까?

목차
Kotlin Coroutines 시리즈
- 코루틴이란 무엇인가?
- Kotlin에서 코루틴은 어떻게 구현될까? (현재 글)
이전 글에서는 프로그래밍 언어와 무관하게 일반적인 프로그래밍 관점에서 코루틴이 무엇인가에 대해 글을 써 보았다. 이번 글에서는 Kotlin 언어에서의 코루틴에 대해 글을 써 보려 한다.
Kotlin의 코루틴 구현 #
코루틴은 중간에 정지하고 다른 루틴에게 동작 권한을 넘겨 줄 수 있는 중단 가능한 루틴이라는 것을 이전 글에서 알아보았다.
또한 Kotlin에서는 중단 가능한 루틴은 suspend
함수로 나타낸다.
그렇다면 Kotlin에서 suspend
함수는 어떻게 구현될까?
중단 가능한 루틴의 구현 방식은 여러 가지가 있을 것이다. 그 중 Kotlin에서 채택한 방법은 컨티뉴에이션 전달(Continuation Passing) 방식이다. 이는 모든 중단 가능한 함수가 컨티뉴에이션(Continuation) 이라는 객체를 매개변수로 받고, 자신의 동작을 처리한 뒤 다음 함수를 호출하며 컨티뉴에이션을 넘겨주는 방식이다.
예를 들어 Kotlin에서 중단 가능한 함수는 suspend
키워드를 붙인다.
suspend
함수를 코드로 작성했을 때와 그 코드를 컴파일러가 이해하는 방식으로 놓고 비교해보자면 아래와 비슷할 것이다.
// 코드 작성 시
suspend fun getUserInfo(): User
suspend fun setUserInfo(user: User)
// 컴파일러가 이해하는 방식
fun getUserInfo(continuation: Continuation): Any
fun setUserInfo(user: User, continuation: Continuation): Any
위의 코드처럼 컴파일 시 suspend
함수들에게 매개변수로 컨티뉴에이션이 추가된다.
달라진 점을 살펴보자면,
- 중단 가능한 함수가 일반 함수로 바뀌었다.
- 매개변수로
Continuation
이 추가되었다. - 반환 타입이
Any
로 바뀌었다.
정도로 볼 수 있다.
우선 suspend
키워드가 지워지고 일반적인 함수로 바뀐 것은 컴파일러가 중단 가능한 함수도 일반적인 함수처럼 다루기 위함일 것이다.
대신 모든 suspend
함수들은 Continuation
을 매개변수로 받게 된다.
Continuation
에는 중단 가능한 함수가 어디까지 실행되었는지나 실행되던 중 가지고 있던 여러 가지 값 등 중단 가능한 함수의 상태를 저장한다.
상태를 저장해 두어야 이후 재개되었을 때 진행하던 곳 부터 다시 실행될 수 있기 때문이다.
마지막으로 반환 타입이 Any
가 되었는데, 중단 가능한 함수는 결과가 도출되기 전에 멈출 수 있기 때문이다.
중간에 중단된 경우 아직 결과가 나오지 않았기 때문에 결과값 대신 함수가 중단되었다를 나타내는 특정한 값을 반환한다.
그렇다면 어떻게 중단한 지점부터 다시 재개할 수 있을까?
Kotlin 컴파일러는 suspend
함수를 컴파일 할 때, 중단 가능한 지점들에 추가 코드를 생성한다.
suspend fun count() {
var counter = 0
delay(1000)
counter++
}
위 코드는 count
라는 suspend
함수 안에서 delay
라는 또다른 suspend
함수를 호출한다.
그렇다면 count
에서 중단 가능한 지점은 delay
가 호출되는 지점일 것이다.
이 코드를 컴파일러가 아래와 같이 변경한다.
fun count(continuation: Continuation): Any {
var counter = continuation.counter
if (continuation.label == 0) {
counter = 0
continuation.counter = counter
continuation.label = 1
if (delay(1000, continuation) == COROUTINE_SUSPENDED) {
return COROUTINE_SUSPENDED
}
} else if (continuation.label == 1) {
counter++
return Unit
} else {
error()
}
}
컴파일러는 중단 지점을 label
이라는 것으로 표시한다.
일련의 작업이 끝나면 Continuation
의 label
을 다음 라벨로 업데이트하고, 다른 suspend
함수를 호출하며 Continuation
을 전달한다.
이후 다시 호출되었을 때, Continuation
에 저장된 상태를 다시 가져오고 label
에 따라 중단된 지점 이후부터 작업을 수행한다.
모든 작업이 종료되면 결과를 반환한다.
여기까지 Kotlin에서 중단 가능한 함수는 Continuation
이라는 특수한 객체를 매개변수로 받도록 변경된다는 것을 알았다.
또한 Continuation
을 적절하게 활용해 상태를 저장하고 복원하면서 동작한다는 것도 알게 되었다.
하지만 여기서 궁금한 점이 하나 생겼다.
일반 함수에서 어떻게 중단 가능한 함수에게 Continuation
을 전달할까?
일반 함수와 중단 가능한 함수의 연결 #
Kotlin에서 중단 가능한 함수는 Continuation
을 매개변수로 받는 함수로 변환된다.
다른 의미로 말하면 Continuation
이 전달될 수 있는 상황에서만 중단 가능한 함수 호출이 가능하다는 것이다.
그리고 Kotlin에서는 중단 가능한 함수 안에서만 Continuation
이 전달된다.
즉, 일반 함수에서는 중단 가능한 함수를 호출할 수 없다.
Kotlin에서 중단 가능한 함수는 코루틴 스코프(CoroutineScope) 안에서 실행 가능하다.
코루틴 스코프는 중단 가능한 함수의 생명주기를 관리하고, 스코프 안에서 만들어진 모든 코루틴을 자식 스코프로 가진다.
코루틴 스코프는 코루틴 빌더(Coroutine Builder) 라는 것을 통해 만들어질 수 있는데,
kotlinx.coroutines
라이브러리에서는 대표적으로 3가지 코루틴 빌더를 제공한다.
CoroutineScope.launch
: 기본적인 코루틴 빌더로, 호출하는 즉시 코루틴이 시작된다. 코루틴 스코프 안에서 새로운 코루틴을 만드는 데 사용한다.CoroutineScope.async
:launch
와 유사하지만 값을 반환하는 코루틴 빌더다.async
는 호출되는 즉시 코루틴이 시작되고 그 결과가Deferred<T>
에 저장된다. 실행 결과는Deferred<T>
에 저장되고await
을 호출하면 저장되었던 값T
를 반환한다. 만약 아직 결과가 나오기 전이라면 기다린다.runBlocking
: 특수한 경우에만 사용하게 되는 코루틴 빌더로, 코루틴을 시작하면서 스레드를 블로킹(Blocking)한다. 따라서runBlocking
을 호출한 스레드는 코루틴 작업이 끝날 때 까지 멈춘다. 스레드를 블로킹하는 특징 때문에 아무데서나 사용하면 문제가 생기기 쉬우며 프로그램 종료를 막기 위해 메인 함수에서 사용하거나 유닛 테스트 등 특별한 경우에 사용한다. 또한runBlocking
으로 만든 코루틴 스코프는 부모 스코프를 가질 수 없고 자신이 항상 최상위 부모 스코프가 된다.
fun main() = runBlocking {
launch {
// action
}
val deferredResult = async {
// action
}
val result = deferredResult.await()
println("result: $result")
}
구조화된 동시성 #
코루틴 스코프 안에서 생성 된 코루틴들은 자식 스코프가 된다는 것이 앞서 언급되었다. 이처럼 여러 개의 코루틴을 하나로 묶어 관리하는 것을 구조화된 동시성(Structured Concurrency) 이라고 부른다. 이 구조를 통해서 아래와 같은 특징을 얻을 수 있다.
- 모든 코루틴은 특정한 범위(스코프) 내에서 실행된다.
- 새로 만들어진 코루틴은 그 스코프를 만든 부모 코루틴의 자식이 된다.
- 자식 코루틴의 생명주기는 부모 코루틴에 종속된다.
또한 부모 코루틴과 자식 코루틴은 아래과 같은 특징을 가진다.
- 자식 코루틴은 기본적으로 부모 코루틴의 컨텍스트를 상속한다.
- 부모 코루틴은 모든 자식 코루틴이 완료될 때 까지 종료하지 않고 기다린다.
- 부모 코루틴이 취소되면 모든 자식 코루틴도 취소된다.
- 자식 코루틴에서 에러가 발생하면 부모 코루틴으로 전파되고 다른 자식 코루틴들도 취소된다.
이렇게 Kotlin의 코루틴은 여러 코루틴을 구조화된 방식으로 관리해 취소 관리, 에러 처리, 생명주기 관리를 효과적으로 할 수 있도록 디자인되었다.