본 글은 조세영님의 <코틀린 코루틴의 정석>을 읽고 이를 바탕으로 작성되었습니다.
구조화된 동시성의 원칙이란 비동기 작업을 구조화함으로서 비동기 프로그래밍을 보다 안정적이고 예측할 수 있게 만드는 원칙이다. 코루틴 또한 동시성의 원칙을 사용해 비동기 작업인 코루틴을 부모-자식 관계로 구조화하고, 이를 통해 코루틴이 더욱 안전하게 사용될 수 있다. 오늘은 이러한 코루틴의 구조화에 대해서 알아보고 그와 함께 CoroutineScope에 대해서도 알아보자.
구조화된 코루틴
구조화된 코루틴은 다음과 같은 특징을 갖는다
- 부모 코루틴의 실행 환경이 자식 코루틴에게 상속된다
- 작업을 제어하는 데 사용된다
- 부모 코루틴이 취소되면 자식 코루틴도 취소된다
- 부모 코루틴은 자식 코루틴이 완료될 때까지 대기한다
- CoroutineScope를 사용해 코루틴이 실행되는 범위를 제한할 수 있다
실행 환경 상속
부모 코루틴이 자식 코루틴을 생성하면, 부모 코루틴의 CoroutineContext가 자식 코루틴에게 전달된다. 이때, 자식 코루틴을 생성하는 코루틴 빌더 함수로 새로운 CoroutineContext 객체가 전달되면 부모로부터 상속된 CoroutineContext는 자식 코루틴 빌더 함수로 전달된 CoroutineContext 객체 구성 요소로 덮어씌워진다.
요약해보면 자식이 새로운 코루틴 빌더를 만들지 않는 이상 부모 코루틴의 실행 환경은 자식에게 전달된다. 그렇다면 Job또한 상속될까 ?
fun main() = runBlocking<Unit> { // 부모 코루틴 생성
val runBlockingJob = coroutineContext[Job]
launch { // 자식 코루틴 생성
val launchJob = coroutineContext[Job]
if (runBlockingJob === launchJob) {
println("runBlocking으로 생성된 Job과 launch로 생성된 Job이 동일합니다")
} else {
println("runBlocking으로 생성된 Job과 launch로 생성된 Job이 다릅니다")
}
}
}
/*
// 결과:
runBlocking으로 생성된 Job과 launch로 생성된 Job이 다릅니다
*/
위의 예제의 결과를 보면 알 수 있듯이 답은 아니다. 생각해보면 이유는 간단하다. 코루틴 빌더함수는 호출 시에 Job객체를 새롭게 생성하는데, 부모가 job을 상속하면 개별 코루틴의 제어가 어려워지기 때문이다. 그렇다면 두 Job 사이엔 아무 관계가 없을까 ?
다음은 생성되는 Job 객체의 프로퍼티이다.
Job
Job 프로퍼티 | 타입 | 설명 |
parent | Job? | 부모 코루틴 없을 수도 (있으면 유일)/ 있을 수도 |
children | Sequence<Job> | 하나의 코루틴이 복수의 자식 코루틴을 가질 수 있다 |
부모의 Job과 자식의 Job이 같은건 아니지만 위 표를 보고 알 수 있듯이 자식 코루틴의 Job은 parent 프로퍼티를 통해 부모에 대한 참조를, 부모의 Job은 children 프로퍼티를 통해 자식에 대한 참조를 가진다.
코루틴의 구조화와 작업 제어
구조화된 코루틴은 다음과 같은 특징을 가진다.
- 코루틴으로 취소가 요청되면 자식 코루틴으로 전파되어 자식도 같이 취소된다
- 자식 방향으로만 전파되며, 부모 코루틴으로는 취소가 전파되지 않는다
- 부모 코루틴은 모든 자식 코루틴이 실행 완료되어야 완료될 수 있다
코루틴의 상태 : 실행 완료 중
이전 포스팅 에서 코루틴의 상태를 생성-실행중-실행완료-취소중-취소완료로 언급한 적이 있다. 여기서 부모 코루틴이 자식 코루틴의 실행 완료를 대기하는 상황을 배웠으니, 이제 그에 해당하는 상태 하나를 추가해보자. 바로 '실행 완료 중'이다.
상태 | isActive | isCancelled | isCompleted |
생성 | false | false | false |
실행완료중 | true | false | false |
실행중 | true | false | false |
실행완료 | false | false | true |
취소중 | false | true | false |
취소완료 | false | true | true |
사실 실행완료중은 실행중과 같은 Job 상태 값을 가지고, 일반적으로는 구분 없이 사용한다. 하지만 코루틴의 부모-자식 간의 구조화와 작업 제어를 한번 더 짚고 넘어가는 겸, 알아두도록 하자.
CoroutineScope 사용해 코루틴 관리하기
CoroutineScope 객체는 자신의 범위 내에서 생성된 코루틴들에게 실행 환경을 제공하고, 이들의 실행 범위를 관리하는 역할을 한다. CoroutineScope는 다음과 같은 방법으로 생성할 수 있다.
CoroutineScope 인터페이스 구현을 통한 생성
class CustomCoroutineScope : CoroutineScope {
override val coroutineContext: CoroutineContext = Job() +
newSingleThreadContext("CustomCoroutineScope")
}
fun main() {
val coroutineScope = CustomCoroutineScope() // CustomCoroutineScope 인스턴스화
coroutineScope.launch {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행 완료")
}
Thread.sleep(1000L)
}
CoroutineScope 함수를 통한 생성
fun main() {
val coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope.launch {
delay(100L) // 100밀리초 대기
println("[${Thread.currentThread().name}] 코루틴 실행 완료")
}
Thread.sleep(1000L)
}
이렇게 CoroutineScope를 통해 코루틴이 생성되면, 코루틴은 그로부터 실행 환경 (CoroutineContext)를 받는다. 아래 코루틴 빌더 launch 내부 코드를 보면 알 수 있듯이, 수신 객체인 CoroutineScope로부터 CoroutineContext를 받고, Job 객체를 새로 생한다. 즉, 우리가 지금까지 launch 함수의 람다식에서 this.CoroutineContext를 통해 실행 환경에 접근할 수 있었던 이유는 CoroutineScope가 수신 객체였기 때문이다.
public fun CoroutineScope.launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job {
val newContext = newCoroutineContext(context)
val coroutine = if (start.isLazy)
LazyStandaloneCoroutine(newContext, block) else
StandaloneCoroutine(newContext, active = true)
coroutine.start(start, coroutine, block)
return coroutine
}
만약 CoroutineScope에서 벗어나고 싶으면 새로운 CoroutineScope를 생성하고, 그 스코프에서 코루틴을 생성하면 된다. 또, 취소하고 싶은 경우는 this.cancel()을 통해 Job객체에 접근해 취소할 수 있다. 이때, 앞서 알아보았던 코루틴의 전파와 같이 CoroutineScope 의 취소 또한 부모를 취소하면 자식들도 취소된다.
구조화와 Job
Job 생성 함수를 호출해 Job 객체를 생성할 수 있으며, 이를 사용해서 코루틴의 구조화를 유지하거나 깰 수 있다. 이때, Job 생성 함수를 통해 생성된 Job 객체는 자동으로 실행되지 않으므로 complete()를 호출해 명시적으로 완료시켜야 한다.
'코틀린 > Kotlin Coroutine' 카테고리의 다른 글
[Kotlin][Coroutine] Flow 만들기 (0) | 2024.08.21 |
---|---|
[Kotlin][Coroutine] 일시 중단 함수의 이해 (0) | 2024.08.19 |
[Kotlin][Coroutine] Flow의 이해 (0) | 2024.07.06 |
[Kotlin][Coroutine] CoroutineContext (0) | 2024.07.02 |
[Kotlin][Coroutine] Async, Await, Deferred, withContext (0) | 2024.07.02 |