코루틴을 생성하는 데 사용하는 함수 → 코루틴 빌더
코루틴 빌더는 코루틴을 만들고 코루틴을 추상화한 Job 객체를 생성
코루틴 빌더인 launch 함수를 호출하면 코투린이 만들어지고, Job 객체가 생성돼 반환
반환된 Job 객체는 코루틴의 상태를 추적하고 제어하는데 사용됨
fun main() = runBlocking<Unit> {
val job: Job = launch(Dispatchers.IO) { //Job 객체 반환
println("[${Thread.currentThread().name}] 실행")
}
}
코루틴 → 일시 중단할 수 있는 작업
코루틴을 추상화한 Job 객체는 이에 대응해 코루틴을 제어할 수 있는 함수와 코루틴의 상태를 나타내는 상태 값들을 외부에 노출
4장에서 다루는 내용
- join, joinAll 함수를 사용한 코루틴 간 순차 처리
- CoroutineStart.LAZY를 사용한 코루틴 지연 시작
- 코루틴 실행 취소
- 코루틴의 상태
코루틴 간에는 순차 처리가 필요한 경우가 종종 발생
Job 객체는 순차 처리가 필요한 상황을 위해 join함수를 제공해 먼저 처리돼야 하는 코루틴의 실행이 완료될 때까지 호출부의 코루틴을 일시 중단하도록 만들 수 있음
네트워크 요청 시 인증 토큰이 필요한 상황
토큰 업데이트 작업과 네트워크 요청 작업에 순차 처리가 적용되지 않은 코드
fun main() = runBlocking<Unit> {
val updateTokenJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 토큰 업데이트 시작")
delay(100L)
println("[${Thread.currentThread().name}] 토큰 업데이트 완료")
}
val networkCallJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 네트워크 요청")
}
}
/*
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-3 @coroutine#3] 네트워크 요청
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 완료
*/

Job 객체의 조인 함수를 사용하면 코루틴 간에 순차 처리가 가능함
JobA 코루틴이 완료된 이후에 JobB 코루틴이 실행돼야 한다면 JobB 코루틴이 실행되기 전에 JobA 코루틴에 join 함수를 호출하면 됨
fun main() = runBlocking<Unit> {
val updateTokenJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 토큰 업데이트 시작")
delay(100L)
println("[${Thread.currentThread().name}] 토큰 업데이트 완료")
}
updateTokenJob.join() // updateTokenJob이 완료될 때까지 runBlocking 코루틴 일시 중단
val networkCallJob = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 네트워크 요청")
}
}
/*
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 완료
[DefaultDispatcher-worker-3 @coroutine#3] 네트워크 요청
*/

“join 함수를 호출한 코루틴은 join의 대상이 된 코루틴이 완료될 때까지 일시 중단됨”
join 함수는 일시 중단이 가능한 지점(코루틴 등)에서만 호출할 수 있음
join 함수는 join을 호출한 코루틴만 일시 중단한다.
join 함수는 join함수를 호출한 코루틴을 제외하고 이미 실행중인 다른 코루틴을 일시 중단하지 않음
fun main() = runBlocking<Unit> { val updateTokenJob = launch(Dispatchers.IO) { println("[${Thread.currentThread().name}] 토큰 업데이트 시작") delay(100L) println("[${Thread.currentThread().name}] 토큰 업데이트 완료") } val independentJob = launch(Dispatchers.IO) { println("[${Thread.currentThread().name}] 독립적인 작업 실행") } updateTokenJob.join() // updateTokenJob이 완료될 때까지 runBlocking 코루틴 일시 중단 val networkCallJob = launch(Dispatchers.IO) { println("[${Thread.currentThread().name}] 네트워크 요청") } } /* [DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 시작 [DefaultDispatcher-worker-2 @coroutine#2] 독립적인 작업 실행 [DefaultDispatcher-worker-2 @coroutine#2] 토큰 업데이트 완료 [DefaultDispatcher-worker-3 @coroutine#3] 네트워크 요청 */independentJob은 updateTokenJob.join()이 호출되더라도 updateTokenJob이 끝날 때까지 기다리지 않고 실행되는 것을 확인할 수 있음
실제 개발 시에는 서로 독립적인 여러 코루틴을 병렬로 실행한 후 실행한 요청들이 모두 끝날 때까지 기다렸다가 다음 작업을 진행하는 것이 효율적
SNS에 이미지를 업로드 하는 경우
joinAll 함수는 복수의 코루틴 실행이 모두 끝날 때까지 호출부의 코루틴을 일시 중단
joinAll 함수의 내부 동작 → 가변 인자로 Job 타입의 객체를 받은 후 각 Job 객체에 대해 모두 join 함수를 호출
public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach {
it.join()
}
fun main() = runBlocking<Unit> {
val convertImageJob1: Job = launch(Dispatchers.Default) {
Thread.sleep(1000L)
println("[${Thread.currentThread().name}] 이미지 1 변환 완료")
}
val convertImageJob2: Job = launch(Dispatchers.Default) {
Thread.sleep(1000L)
println("[${Thread.currentThread().name}] 이미지 2 변환 완료")
}
joinAll(convertImageJob1, convertImageJob2)
val uploadImageJob: Job = launch(Dispatchers.IO) {
println("[${Thread.currentThread().name}] 이미지1, 2 업로드")
}
}
/*
[DefaultDispatcher-worker-1 @coroutine#2] 이미지1 변환 완료
[DefaultDispatcher-worker-2 @coroutine#3] 이미지1 변환 완료
[DefaultDispatcher-worker-1 @coroutine#4] 이미지1, 2 업로드
*/

launch 함수를 통한 코루틴 생성 → 사용할 수 있는 스레드가 있을 경우 곧바로 실행
코루틴을 미리 생성한 후 나중에 실행해야 할 수 있음
코루틴 라이브러리는 생성된 코루틴을 지연 시작(Lazy Start)할 수 있는 기능을 제공함
지연 시작이 적용된 코루틴은 생성 후 대기 상태에 놓이며, 실행을 요청하지 않으면 실행되지 않음
코루틴을 지연 시작하기 위해서는 launch 함수의 start 인자로 CoroutineStart.LAZY를 넘겨 옵션을 적용해야 함
val lazyJob: Job = launch(start = CoroutineStart.LAZY) { // 지연 코루틴
...
}
lazyJob.start() // 실행 요청
코루틴 실행 도중 필요가 없어지면 즉시 취소해야 함 → 스레드를 점유해 성능 하락이 발생할 수 있음
코루틴 라이브러리의 Job 객체는 코루틴을 취소할 수 있는 cancel 함수를 제공함
fun main() = runBlocking<Unit> {
val longJob: Job = launch(Dispatchers.Default) {
repeat(10) { repeatTime ->
delay(1000L)
println("반복횟수 ${repeatTime}")
}
}
delay(3500L)
longJob.cancel() // 코루틴 취소
}
cancel() 함수 → 코루틴을 즉시 취소하는 것이 아닌 Job 객체의 취소 확인용 플래그를 변경해 이 코루틴이 취소되어야 한다고 알리는 함수
cancel 함수를 호출한다고 해당 코루틴이 즉시 취소되는 것이 아님
취소에 대한 순차성 보장을 위해 Job 객체는 cancelAndJoin 함수를 제공
cancelAndJoin 함수를 사용하면 코루틴의 취소가 완료될 때까지 호출부의 코루틴이 일시 중단됨
fun main() = runBlocking<Unit> {
val longJob : Job = launch(Dispatchers.Default) {
...
}
longJob.cancelAndJoin()
executeAfterJobCancelled()
}
cancel 함수 & cancelAndJoin 함수 → Job 객체 내부에 있는 취소 확인용 플래그를 바꾸기만 하며, 코루틴이 이 플래그를 확인하는 시점에 취소됨
코루틴이 취소를 확인하는 시점은 일반적으로 일시 중단 지점이나 코루틴이 실행을 대기하는 시점
fun main() = runBlocking<Unit> {
val whileJob: Job = launch(Dispatchers.Default) {
while(true) {
println("작업 중")
}
}
delay(100L)
whileJob.cancel()
}
whileJob 코루틴은 코루틴의 취소를 확인할 수 있는 시점이 없어 취소가 요청됐음에도 계속해서 실행됨
이런 코루틴을 취소하는 방법들에 대해 알아본다.
delay 함수 → 일시 중단 함수 (suspend fun)
코루틴은 일시 중단 시점에 취소를 확인하기 때문에 while문 내부에 delay를 직접 선언하면 일시 중단 시점을 만들어 코루틴을 취소할 시점을 만들 수 있음
fun main() = runBlocking<Unit> {
val whileJob: Job = launch(Dispatchers.Default) {
while(true) {
println("작업 중")
delay(1L)
}
}
delay(100L)
whileJob.cancel()
}
이 방법은 while문이 반복될 때마다 작업을 강제로 1밀리초 동안 일시 중단시킨다는 점에서 효율적이지 않다.
yield → 양보
yield 함수가 호출되면 코루틴은 자신이 사용중이던 스레드의 사용을 중단하면서 작업을 일시 중단함
작업이 일시 중단되기 때문에 코루틴 취소를 확인할 수 있는 시간이 생김
fun main() = runBlocking<Unit> {
val whileJob: Job = launch(Dispatchers.Default) {
while(true) {
println("작업 중")
yield()
}
}
delay(100L)
whileJob.cancel()
}
yield() 함수도 while문이 한 번 돌 때마다 스레드 사용이 양보되면서 일시중단되는 문제가 있음
경량 스레드라고 해도 매번 일시 중단되는 것은 비효츌적임
CoroutineScope.isActive → 코루틴이 활성화됐는지 확인할 수 있는 Boolean 타입의 파라미터
코루틴 취소가 요청되면 isActive 프로퍼티의 값이 false로 바뀜
fun main() = runBlocking<Unit> {
val whileJob: Job = launch(Dispatchers.Default) {
while(this.isActive) {
println("작업 중")
}
}
delay(100L)
whileJob.cancel()
}
이 방법을 사용하면 코루틴이 잠시 멈추지도 않고 스레드 사용을 양보하지도 않으며 계속해서 작업을 이어갈 수 있어 효율적임
코루틴 내부의 작업이 일시 중단 지점 없이 계속된다면 명시적으로 코루틴이 취소됐는지 확인하는 코드를 넣어줌으로써 코루틴을 취소할 수 있도록 만들어줘야 함
그렇지 않으면 코루틴 취소가 동작하지 않을 수 있음

코루틴은 총 6개의 상태를 가짐
Job 객체는 코루틴이 어떤 상태에 있는지 나타내는 상태 변수들을 외부로 공개
다만 Job 객체는 코루틴을 추상화한 객체이므로 노출하는 상태 변수들은 코루틴의 상태를 간접적으로만 나타냄
코루틴의 각 상태애서 Job 객체가 노출하는 상태 변수들의 상태 값
코루틴이 생성만 되고 실행되지 않은 상태
생성 상태의 코루틴을 생성하려면 코루틴 빌더의 start 인자로 CoroutineStart.Lazy를 넘겨 지연 시작이 적용된 코루틴을 생성해야 함
fun main() = runBlocking<Unit> {
val job: Job = launch(start = CoroutineStart.LAZY) {
delay(1000L)
}
printJobState(job)
}
/*
Job State
isActive >> false
isCancelled >> false
isCompleted >> false
*/
코루틴 빌더로 코루틴을 생성하면 CoroutineDispatcher에 의해 스레드로 보내져 실행됨
이때 코루틴이 실행되고 있는 상태를 ‘실행 중’ 상태라고 부름
fun main() = runBlocking<Unit> {
val job: Job = launch {
delay(1000L)
}
printJobState(job)
}
/*
Job State
isActive >> true
isCancelled >> false
isCompleted >> false
*/
실행 중인 코루틴이 모두 정상적으로 실행돼 실행 완료되면 코루틴은 실행 완료 상태로 변경됨
fun main() = runBlocking<Unit> {
val job: Job = launch {
delay(1000L)
}
delay(2000L)
printJobState(job)
}
/*
Job State
isActive >> false
isCancelled >> false
isCompleted >> true
*/
취소가 요청됐으나 취소되지 않은 상태인 ‘취소 중’ 상태
단순히 취소 요청을 하면 코루틴은 곧바로 취소 중 상태에서 취소 완료 상태로 변환하므로 취소르르 확인할 수 있는 시점이 없는 코루틴을 생성하고 취소를 요청해야 취소 중 상태를 확인할 수 있음
fun main() = runBlocking<Unit> {
val job: Job = launch(Dispatchers.Default) {
while(true) {
//
}
}
job.cancel()
printJobState(job)
}
/*
Job State
isActive >> false
isCancelled >> false
isCompleted >> true
*/
여기서 중요한 점이 취소가 요청되면 실제로는 코드가 실행 중이더라도 코루틴이 활성화된 상태로 보지 않는다는 점임
코루틴은 코루틴 취소가 요청되고 취소 요청이 확인되는 시점에 취소가 완료됨
fun main() = runBlocking<Unit> {
val job: Job = launch {
delay(5000L)
}
job.cancelAndJoin()
printJobState(job)
}
/*
Job State
isActive >> false
isCancelled >> true
isCompleted >> true
*/
코루틴 라이브러리 1.7.2 버전을 기준으로 Job 구현체의 toString 함수를 통해 코루틴의 상태값이 toString 문자열에 포함되도록 만들어져있음