코루틴 빌더는 코루틴을 생성하는 함수이다. 대표적인 예로 runBlacking, launch 등이 존재한다.
이러한 코루틴 빌더 함수는 코루틴을 만들고, 코루틴을 추상화한 Job 객체를 생성한다. launch 함수 또한 코루틴 빌더이므로 다음과 같이 launch 함수를 호출하면 코루틴이 만들어지고, Job 객체가 생성돼 반환된다.
반환된 Job 객체는 코루틴의 상태를 추적하고 제어한다.
fun main() = runBlocking<Unit> {
val job:Job = launch(Dispatchers.IO) {} // Job 객체 반환
}
코루틴을 순차 처리해야되는 상황은 분명 필요하다. 캐싱된 토큰 값을 업데이트하고 그 이후에 네트워크 요청을 해야 되는 상황에서 코루틴을 순차적으로 처리해야 한다.
Job 객체의 join 함수를 사용하면 코루틴 간에 순차 처리가 가능한다.
fun main() = runBlocking<Unit> {
val updateTokenJob = launch(Dispatchers.IO) {
println("~")
delay(100L)
println("~")
}
updateTokenJob.join() // updateTokenJob이 완료될 때까지 runBlocking 코루틴이 일시 중단
val networkCallJob = launch(Dispatchers.IO) {
println("~")
}
}
Job 객체의 join 함수를 호출하면 join의 대상이 된 코루틴의 작업이 완료될 때까지 join 함수를 호출한 코루틴이 일시중단 됩니다.
위 코드에서는 updateTokenJob 코루틴의 작업이 끝날 때까지 runBlocking 코루틴이 일시 중단한다. updateTokenJob 코루틴의 작업이 끝나면 runBlocking 코루틴이 재개돼 networkCallJob 코루틴을 실행한다.
이런 방식으로 코루틴 간의 순차 처리에 join 함수를 이용할 수 있다.
중요한 point
join 함수를 호출한 코루틴이 join의 대상이 된 코루틴의 작업이 완료될 때까지 일시중단 된다는 점
joinAll() 을 통해 하나의 코루틴을 기다릴뿐만 아니라 여러 코루틴을 기다리고 순차적으로 처리할 수 있다.
예를 들어 복수개의 이미지를 선택하고 이미지를 모두 변환한 후 업로드 작업을 진행하는 기능을 만들어야 한다. 이미지 개수가 3개라고 하면, 코루틴을 하나만 만들어 한 번에 이미지를 한나씩 변환하기보다 코루틴을 세개 만들어 각 이미지 변환 작업을 병렬로 실행한 후 결과를 취합해 업로드 작업을 실행하는 것이 효율적이다.
fun main() = runBlocking<Unit> {
val convertImageJob1 = launch(Dispatchers.Default) {
delay(100L)
}
val convertImageJob2 = launch(Dispatchers.Default) {
delay(100L)
}
joinAll(convertImageJob1, convertImageJob2)
// 이미지1과 이미지2가 변활될 때까지 runBlocking 코루틴이 일시 중단
val uploadImageJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}], 이미지 업로드")
}
}
joinAll() 을 통해 복수개의 코루틴을 순차적으로 처리할 수 있다.
실행 중인 코루틴이 실행될 필요가 없어지면 즉시 코루틴을 취소해야한다. 예를 들어 파일 변환 작업이 오래 걸리는 코루틴이 실행되고 있을 때 사용자가 취소하기 버튼을 눌렀을 때 작업이 취소됐음에도 코루틴이 계속 실행된다면 의미 없는 작업에 스레드가 계속 사용되기 떄문에 애플리케이션의 성능을 떨어뜨린다.
이 문제를 해결하기 위해 Job 객체는 코루틴을 취소할 수 있는 cancel 함수를 제공한다.
fun main() = runBlocking<Unit> {
val longJob = launch(Dispatchers.Default) {
repeat(10) {
delay(1000L)
println("~~")
}
}
delay(3500L)
longJob.cancel()
}
위의 코드에서는 longJob 코루틴이 실행되다가 3.5초가 지난 이후에 코루틴이 취소되는 결과를 보인다.
그러나 여기서 주의할 점이 있다.
cancel 함수를 호출하면 코루틴이 즉시 취소되는 것이 아니라 Job 객체 내부의 취소 확인용 플래그를 '취소 요청됨'으로 변경함으로써 코루틴이 취소해야 한다는 것만 알린다. 미래의 어느 시점에 코루틴의 취소가 요청됐는지 체크하고 취소한다.
fun main() = runBlocking<Unit> {
val whileJob = launch(Dispatchers.Default) {
while(True) {
println("코루틴 무한 실행중")
}
}
delay(1000L)
longJob.cancel()
}
위의 코드는 whileJob 코루틴이 실행되고 1초가 지난 후에 cancel 함수로 인해 whileJob 코루틴이 취소될 것 처럼 보인다. 그러나 whileJob 코루틴은 취소되지 않고 무한히 실행된다.
이러한 이유는 cancel 함수로 whileJob의 취소 확인용 플래그의 상태가 '취소 요청됨'으로 변경되었지만 whileJob 코루틴이 취소 확인용 플래그의 상태를 체크할 시간이 없어서 취소가 되지 않은 것이다.
그렇다면 언제 취소 확인용 플래그를 체크할 수 있을까?
코루틴은 일반적으로 실행 대기 시점이나, 일시 중단 시점에 취소를 확인한 후 취소된다.
위의 코드에서는 코루틴이 취소 확인용 플래그를 체크할 수 있도록 만드는 3가지 방법이 있다.
fun main() = runBlocking<Unit> {
val whileJob = launch(Dispatchers.Default) {
while(True) {
println("코루틴 무한 실행중")
delay(1L)
}
}
delay(100L)
longJob.cancel()
}
delay 함수는 일시 중단 함수로 선언돼 시간만큼 호출부의 코루틴을 일시 중단하게 만든다. 코루틴은 일시 중단된 시점에 취소 상태를 확인하기 때문에 여기서는 1ms마다 취소 상태를 체크한다.
하지만 매번 취소 상태를 확인하기 위해 매번 1ms 마다 일시 중단시키는 것으로 인해 분명 성능 저하가 일어날 것이다.
fun main() = runBlocking<Unit> {
val whileJob = launch(Dispatchers.Default) {
while(True) {
println("코루틴 무한 실행중")
yield()
}
}
delay(100L)
longJob.cancel()
}
yield는 직영ㄱ하면 '양보'라는 뜻으로 yield 함수가 호출되면 코루틴은 자신이 사용하던 스레드를 양보한다. 스레드 사용을 양보한다는 것은 스레드 사용을 중단한다는 뜻이므로 yield를 호출한 코루틴이 일시 중단되며 이 시점에 취소됐는지 체크가 일어난다. 위 코드에서는 Dispatchers.Default를 사용하는 코루틴이 whileJob밖에 없으므로 whileJob 코루틴이 잠깐 일시 중단된 이후 곧바로 재개하지만 그 사이에 취소 상태를 체크한다.
하지만 while 문을 돌면서 매번 스레드를 양보하면서 일시 중단되는 것은 비효율적이다.
fun main() = runBlocking<Unit> {
val whileJob = launch(Dispatchers.Default) {
while(this.isActive) {
println("코루틴 무한 실행중")
}
}
delay(100L)
longJob.cancel()
}
코루틴이 활성화되어 있는지 확인할 수 있는 Boolean 타입의 프로퍼티인 isActive 를 제공한다. 작업이 실행 중이면 True, 코루틴에 취소가 요청되면 False로 바뀐다.
isActive를 사용하면 매번 코루틴을 양보하거나 일시 중단하여 취소 상태를 체크하는 성능 저하를 가져오지 않는다.
코루틴에서 cancel을 호출하고 다른 작업을 실행하려고 했을 때 코루틴이 취소가 되지 않고 다른 작업이 실행되는 경우가 있을 것이다. 왜냐하면 cancel 은 취소 상태를 '취소 요청'으로 바꾸는 함수이기 때문이다.
이러한 상황속에서 무조건 코루틴이 취소되고 다음 작업을 실행하는 순차적이 처리를 원할 때 cancelAndJoin 함수를 사용하자.
fun main() = runBlocking<Unit> {
val longJob = launch(Dispatchers.Default) {
// 작업 실행
}
longJob.cancelAndJoin() // longJob이 취소될 때까지 runBlocking 코루틴 일시 중단
excuteAfterJobCancelled() // 코루틴 취소 후 실행돼야 하는 동작
}
cancelAndJoin 함수를 호출하면 cancelAndJoin의 대상이 된 코루틴이 취소가 완료될 때까지 호출부의 코루틴(여기서는 runBlocking)이 일시 중단된다.
즉, longJob 코루틴이 취소 완료될 때까지 runBlocking 코루틴이 일시 중단되기 때문에 longJob 코루틴이 취소 완료된 후에 excuteAfterJobCancelled이 실행되는 것을 보장할 수 있다.

코루틴의 상태는 크게 생성, 실행 중, 실행 완료, 취소 중, 취소 완료로 볼 수 있다.
생성
코루틴 빌더를 통해 코루틴을 생성하면 코루틴은 기본적으로 생성 상태에 놓이며, 자동으로 실행 중 상태로 넘어간다. 만약 생성 상태의 코루틴이 실행 중 상태로 자동으로 변경되는 것을 막고 싶다면 코루틴 빌더의 인자로 CoroutineStart.Lazy를 넘겨 지연 코루틴을 만들다.
실행 중
지연 코루틴이 아닌 코루틴을 만들면 자동으로 실행 중 상태로 시작하게 된다. 코루틴이 실행 중일 때뿐만 아니라 실행된 후에 일시 중단된 때도 실행 중 상태로 본다.
실행 완료
코루틴의 모든 코드가 실행 완료된 경우 실행 완료 상태로 넘어간다.
취소 중
Job.cancel() 을 통해 취소가 요청된 경우 취소 중 상태로 넘어가게 된다. 이는 아직 취소된 상태가 아니기 떄문에 코루틴은 계속해서 실행된다.
취소 완료
코루틴의 취소 확인 시점(일시 중단 등)에 취소가 확인된 경우 취소 완료 상태가 된다. 이 상태에서 코루틴은 더 이상 실행되지 않는다.
코루틴은 이와 같은 상태들을 가질 수 있다. Job 객체는 코루틴이 어떤 상태에 있는지 나타내는 상태 변수들을 외부로 공개한다.
isActive
코루틴이 활성화돼 있는지의 여부, 코루틴이 활성화 되어 있으면 True, 그렇지 않으면 False를 반환한다. 활성화돼 있다는 것은 코루틴이 실행된 후 취소 요청되거나 실행이 완료되지 않은 상태라는 의미이다.
따라서 취소가 요청되거나 실행이 완료된 코루틴은 활성화되지 않은 것으로 ㄷ본다.
isCancelled
코루틴이 취소 요청됐는지의 여부, 취소가 요청되면 True를 반환, 그렇지 않으면 False를 반환, 취소가 되지 않고 취소 요청만 돼도 True 상태인 것을 명심하자
isCompleted
코루틴 실행이 완료됐는지의 여부, 코루틴의 모든 코드가 실행 완료되거나 취소 완료되면 True를 반환, 실행 중인 상태에서는 False를 반환
printJobState() 함수를 직접 만들어 Job의 상태를 볼 수 있다.
fun printJobState(job: Job) {
println("Job State\n" +
"isActive >> ${job.isActive}\n" +
"isCancelled >> ${job.isCancelled}\n" +
"isCompleted >> ${job.isCompleted}")
)
}
사실 Job 구현체들은 toString 함수를 오버라이드해 코루틴의 상태값이 toString 문자열에 포함되도록 만들어져 있다. 따라서 Job 객체를 println 함수를 사용해 출력하면, 코루틴의 상태값이 나오는 것을 볼 수 있다.
fun main() = runBlocking<Unit> {
val job = launch {
delay(5000L)
}
job.cancelAndJoin() // 코루틴 취소 요청 + 취소가 완료될 때까지 대기
println(job) // Job 객체 출력
}
/* 결과
StandaloneCoroutine{Cancelled}@27a5f880
*/
다만 이 문자열은 디버깅용으로 만들어졌기 때문에, 로그를 출력하는 데만 사용하는 것이 좋다.
cancel 함수를 통해 코루틴에 취소 요청을 할 수 있다.cancel 함수는 코루틴을 취소하는 것이 아닌 코루틴의 취소 플래그가 '취소 요청'으로 상태가 변경되고 이를 코루틴이 확인해야 취소한다.cancel 대신 cancelAndJoin 함수를 이용하자