코루틴을 생성하는 데 사용하는 함수 → 코루틴 빌더

코루틴 빌더는 코루틴을 만들고 코루틴을 추상화한 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를 사용한 코루틴 지연 시작
  • 코루틴 실행 취소
  • 코루틴의 상태

1. join을 사용한 코루틴 순차 처리

코루틴 간에는 순차 처리가 필요한 경우가 종종 발생

  • 데이터베이스 박업을 순차적으로 처리
  • 캐싱된 토큰 값이 업데이트된 이후에 네트워크 요청을 해야하는 상황

Job 객체는 순차 처리가 필요한 상황을 위해 join함수를 제공해 먼저 처리돼야 하는 코루틴의 실행이 완료될 때까지 호출부의 코루틴을 일시 중단하도록 만들 수 있음

1-1. 순차 처리가 안 될 경우의 문제

네트워크 요청 시 인증 토큰이 필요한 상황

  • 인증 토큰이 업데이트 된 이후 네트워크 요청이 실행돼야 요청이 정상적으로 처리될 수 있음
  • 인증 토큰이 업데이트 되기 이전에 요청이 실행되면 문제가 생길 수 있음

토큰 업데이트 작업과 네트워크 요청 작업에 순차 처리가 적용되지 않은 코드

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] 토큰 업데이트 완료
*/

  • 토큰 업데이트 시작 이후 업데이트가 완료되기 전에 네트워크 요청이 발생하는 상황 가정
  • 이 코드에서는 인증 토큰 업데이트 작업과 네트워크 요청 작업이 병렬로 실행
  • 실제 네트워크 환경에서는 심각한 오류가 발생할 수 있음
    • 만료된 토큰으로 요청, Authorization 헤더 누락, 중복 토큰 갱신, 요청 실패 후 불필요한 재시도, 사용자 경험 문제, 데이터 불일치 등등…
  • 이런 문제를 해결하려면 updateTokenJob이 모두 완료된 이후 networkCallJob이 실행되어야 함

1-2. join 함수 사용해 순차 처리하기

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] 네트워크 요청
*/

  • Job 객체의 join 함수를 호출하면 join의 대상이 된 코루틴의 작업이 완료될 때까지 join을 호출한 코루틴이 일시 중단됨
  • runBlocking 코루틴이 updateTokenJob.join()을 호출하면 runBlocking 코루틴은 updateTokenJob 코루틴이 완료될 때까지 일시 중단됨
  • 이후 updateTokenJob의 작업이 모두 완료되면 runBlocking 코루틴이 재개돼 networkCallJob 코루틴을 실행함

“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이 끝날 때까지 기다리지 않고 실행되는 것을 확인할 수 있음


2. joinAll을 사용한 코루틴 순차 처리

실제 개발 시에는 서로 독립적인 여러 코루틴을 병렬로 실행한 후 실행한 요청들이 모두 끝날 때까지 기다렸다가 다음 작업을 진행하는 것이 효율적

SNS에 이미지를 업로드 하는 경우

  • 여러 이미지를 한번에 업로드 할 때
  • 코루틴을 하나만 만들어 각 이미지를 순차적으로 변환하는 것 보다
  • 변환할 이미지 개수만큼 코루틴을 생성해 병렬로 처리한 후 결과를 취함하는 것이 효율적

joinAll 함수는 복수의 코루틴 실행이 모두 끝날 때까지 호출부의 코루틴을 일시 중단

2-1. joinAll 함수

joinAll 함수의 내부 동작 → 가변 인자로 Job 타입의 객체를 받은 후 각 Job 객체에 대해 모두 join 함수를 호출

public suspend fun joinAll(vararg jobs: Job): Unit = jobs.forEach {
	it.join()
}

2-2. joinAll 함수 사용해 보기

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 업로드
*/


3. CoroutineStart.LAZY로 코루틴 지연 시작하기

launch 함수를 통한 코루틴 생성 → 사용할 수 있는 스레드가 있을 경우 곧바로 실행

코루틴을 미리 생성한 후 나중에 실행해야 할 수 있음

코루틴 라이브러리는 생성된 코루틴을 지연 시작(Lazy Start)할 수 있는 기능을 제공함

지연 시작이 적용된 코루틴은 생성 후 대기 상태에 놓이며, 실행을 요청하지 않으면 실행되지 않음

코루틴을 지연 시작하기 위해서는 launch 함수의 start 인자로 CoroutineStart.LAZY를 넘겨 옵션을 적용해야 함

val lazyJob: Job = launch(start = CoroutineStart.LAZY) { // 지연 코루틴
	...
}

lazyJob.start() // 실행 요청

4. 코루틴 취소하기

코루틴 실행 도중 필요가 없어지면 즉시 취소해야 함 → 스레드를 점유해 성능 하락이 발생할 수 있음

코루틴 라이브러리의 Job 객체는 코루틴을 취소할 수 있는 cancel 함수를 제공함

4-1. cancel 사용해 Job 취소하기

fun main() = runBlocking<Unit> {
	val longJob: Job = launch(Dispatchers.Default) {
		repeat(10) { repeatTime -> 
			delay(1000L)
			println("반복횟수 ${repeatTime}")
		}
	}
	
	delay(3500L)
	
	longJob.cancel() // 코루틴 취소
}
  • 총 10번의 로그를 찍는 코루틴(longJob)
  • 3500 밀리초 뒤에 코루틴을 취소해 로그가 3번 까지 찍힘

4-2. cancelAndJoin을 사용한 순차 처리

cancel() 함수 → 코루틴을 즉시 취소하는 것이 아닌 Job 객체의 취소 확인용 플래그를 변경해 이 코루틴이 취소되어야 한다고 알리는 함수

cancel 함수를 호출한다고 해당 코루틴이 즉시 취소되는 것이 아님

취소에 대한 순차성 보장을 위해 Job 객체는 cancelAndJoin 함수를 제공

cancelAndJoin 함수를 사용하면 코루틴의 취소가 완료될 때까지 호출부의 코루틴이 일시 중단됨

fun main() = runBlocking<Unit> {
	val longJob : Job = launch(Dispatchers.Default) { 
		...
	}
	
	longJob.cancelAndJoin()
	
	executeAfterJobCancelled()
}
  • longJob 코루틴이 취소될 때까지 runBlocking 코루틴이 일시 중단됨

5. 코루틴의 취소 확인

cancel 함수 & cancelAndJoin 함수 → Job 객체 내부에 있는 취소 확인용 플래그를 바꾸기만 하며, 코루틴이 이 플래그를 확인하는 시점에 취소됨

코루틴이 취소를 확인하는 시점은 일반적으로 일시 중단 지점이나 코루틴이 실행을 대기하는 시점

fun main() = runBlocking<Unit> {
	val whileJob: Job = launch(Dispatchers.Default) {
		while(true) {
			println("작업 중")
		}
	}
	delay(100L)
	whileJob.cancel()
}
  • whileJob 객체의 코루틴이 100밀리초 후에 취소도리 것 같지만 실제로는 취소되지 않음
  • 코루틴 블록 내부에 콜틴의 취소를 확인할 수 있는 시점이 없기 때문임
  • while 문에서 코드가 반복해 실행되고 있어 while문을 벗어날 수 없음
  • while 문 내부에도 일시 중단 시점이 없어 일시 중단이 일어나지 않음

whileJob 코루틴은 코루틴의 취소를 확인할 수 있는 시점이 없어 취소가 요청됐음에도 계속해서 실행됨

이런 코루틴을 취소하는 방법들에 대해 알아본다.

5-1. delay를 사용한 취소 확인

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밀리초 동안 일시 중단시킨다는 점에서 효율적이지 않다.

5-2. yield를 사용한 취소 확인

yield → 양보

yield 함수가 호출되면 코루틴은 자신이 사용중이던 스레드의 사용을 중단하면서 작업을 일시 중단함

작업이 일시 중단되기 때문에 코루틴 취소를 확인할 수 있는 시간이 생김

fun main() = runBlocking<Unit> {
	val whileJob: Job = launch(Dispatchers.Default) {
		while(true) {
			println("작업 중")
			yield()
		}
	}
	delay(100L)
	whileJob.cancel()
}

yield() 함수도 while문이 한 번 돌 때마다 스레드 사용이 양보되면서 일시중단되는 문제가 있음

경량 스레드라고 해도 매번 일시 중단되는 것은 비효츌적임

5-3. CoroutineScope.isActive를 사용한 취소 확인

CoroutineScope.isActive → 코루틴이 활성화됐는지 확인할 수 있는 Boolean 타입의 파라미터

코루틴 취소가 요청되면 isActive 프로퍼티의 값이 false로 바뀜

  • while문의 인자로 this.isActive를 넘기면 코루틴 취소 요청이 발생했을 때 while문이 취소되도록 만들 수 있음
fun main() = runBlocking<Unit> {
	val whileJob: Job = launch(Dispatchers.Default) {
		while(this.isActive) {
			println("작업 중")
		}
	}
	delay(100L)
	whileJob.cancel()
}

이 방법을 사용하면 코루틴이 잠시 멈추지도 않고 스레드 사용을 양보하지도 않으며 계속해서 작업을 이어갈 수 있어 효율적임

코루틴 내부의 작업이 일시 중단 지점 없이 계속된다면 명시적으로 코루틴이 취소됐는지 확인하는 코드를 넣어줌으로써 코루틴을 취소할 수 있도록 만들어줘야 함

그렇지 않으면 코루틴 취소가 동작하지 않을 수 있음


6. 코루틴의 상태와 Job의 상태 변수

코루틴은 총 6개의 상태를 가짐

  • 생성(New)
    • 코루틴 빌더를 통해 코루틴을 생성하면 기본적으로 생성 상태에 놓이며 자동으로 실행 상태로 넘어감
    • 만약 실행 상태로 자동으로 넘어가지 않도록 하고 싶다면 코루틴 빌더의 start 인자로 CoroutineStart.Lazy를 넘겨 지연 코루틴을 만들면 됨
  • 실행 중 (Active)
    • 코루틴이 생성되고 자동으로 진입하는 상태
    • 코루틴이 실행중일 때 뿐만 아니라 실행된 후애 일시 중단된 때도 실행중 상태로 간주함
  • 실행 완료 중 → 구조화된 동시성 (7장에서 다룸)
  • 실행 완료 (Completed)
    • 코루틴의 모든 코드가 실행 완료된 경우 실행 완료 상태로 넘어감
  • 취소 중 (Canceling)
    • Job.cancel() 등으로 코루틴에 취소 요청이 발생했을 경우 취소 중 상태로 넘어감
    • 아직 취소된 상태가 아니기 때문에 코루틴은 계속해서 실행됨
  • 취소 완료 (Cancelled)
    • 코루틴의 취소 확인 시점에 취소가 확인 된 경우 취소 완료 상태가 됨
    • 코루틴이 더 이상 실행되지 않음

Job 객체는 코루틴이 어떤 상태에 있는지 나타내는 상태 변수들을 외부로 공개

다만 Job 객체는 코루틴을 추상화한 객체이므로 노출하는 상태 변수들은 코루틴의 상태를 간접적으로만 나타냄

코루틴의 각 상태애서 Job 객체가 노출하는 상태 변수들의 상태 값

  • isActive
    • 코루틴이 활성화돼 있는지의 여부
    • 활성화 → true / 비활성화 → false
    • 활성화 상태 = 코루틴이 실행된 후 취소가 요청되지 않았거나 실행이 완료되지 않은 상태라는 의미
    • 취소가 요청되었거나 실행이 완료된 상태 → 활성화되지 않은 것으로 봄
  • isCancelled
    • 코루틴이 취소 요청됐는지 여부
    • 취소가 요청되는 즉시 true가 반환됨
    • isCancelled가 true여도 코루틴이 즉시 취소되는 것은 아님
  • isCompleted
    • 코루틴 실행이 완료되었는지 여부
    • 코루틴의 모든 코드가 실행 완료 되거나 취소 완료되면 true를 반환
    • 코루틴이 실행중인 상태에서는 false를 반환

6-1. 생성 상태의 코루틴

코루틴이 생성만 되고 실행되지 않은 상태

생성 상태의 코루틴을 생성하려면 코루틴 빌더의 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
*/
  • 코루틴 생성 후 실행되지 않았으므로 isActive가 false임
  • 취소가 요청되지 않았으므로 isCancelled가 false임
  • 코루틴 실행이 완료되지 않았으므로 isCompleted가 false임

6-2. 실행 중 상태의 코루틴

코루틴 빌더로 코루틴을 생성하면 CoroutineDispatcher에 의해 스레드로 보내져 실행됨

이때 코루틴이 실행되고 있는 상태를 ‘실행 중’ 상태라고 부름

fun main() = runBlocking<Unit> {
	val job: Job = launch {
		delay(1000L)
	}
	printJobState(job)
}

/*
Job State
isActive >> true
isCancelled >> false
isCompleted >> false
*/
  • 코루틴이 실행된 후 취소되거나 완료되지 않았으므로 isActive가 true임
  • 코루틴에 취소가 요청되지 않았으므로 isCancelled가 false임
  • 코루틴이 실행 완료되지 않았으므로 isCompleted가 false임

6-3. 실행 완료 상태의 코루틴

실행 중인 코루틴이 모두 정상적으로 실행돼 실행 완료되면 코루틴은 실행 완료 상태로 변경됨

fun main() = runBlocking<Unit> {
	val job: Job = launch {
		delay(1000L)
	}
	delay(2000L)
	printJobState(job)
}

/*
Job State
isActive >> false
isCancelled >> false
isCompleted >> true
*/
  • 코루틴이 실행 완료돼 활성화된 상태가 아니므로 isActive는 false임
  • 취소가 요청되지 않고 정상적으로 실행 완료됐으므로 isCancelled는 false임
  • 실행이 완료된 것이므로 isCompleted는 true임

6-4. 취소 중인 코루틴

취소가 요청됐으나 취소되지 않은 상태인 ‘취소 중’ 상태

단순히 취소 요청을 하면 코루틴은 곧바로 취소 중 상태에서 취소 완료 상태로 변환하므로 취소르르 확인할 수 있는 시점이 없는 코루틴을 생성하고 취소를 요청해야 취소 중 상태를 확인할 수 있음

fun main() = runBlocking<Unit> {
	val job: Job = launch(Dispatchers.Default) {
		while(true) {
			//
		}
	}
	job.cancel()
	printJobState(job)
}

/*
Job State
isActive >> false
isCancelled >> false
isCompleted >> true
*/
  • 코루틴이 취소 요청됐으므로 화성화돼 있지 않다고 판단해 isActive가 false임
  • 코루틴 취소가 요청 됐으므로 isCancelled는 true임
  • 취소가 완료되지 않았으므로 isCompleted가 false임

여기서 중요한 점이 취소가 요청되면 실제로는 코드가 실행 중이더라도 코루틴이 활성화된 상태로 보지 않는다는 점임

6-5. 취소 완료된 코루틴

코루틴은 코루틴 취소가 요청되고 취소 요청이 확인되는 시점에 취소가 완료됨

fun main() = runBlocking<Unit> {
	val job: Job = launch {
		delay(5000L)
	}
	job.cancelAndJoin()
	printJobState(job)
}

/*
Job State
isActive >> false
isCancelled >> true
isCompleted >> true
*/
  • 취소가 완료돼 코루틴이 활성화되어 있지 않으므로 isActive가 false임
  • 취소 요청 후 취소가 완료되었으므로 isCancelled와 isCompleted가 true임

코루틴 라이브러리 1.7.2 버전을 기준으로 Job 구현체의 toString 함수를 통해 코루틴의 상태값이 toString 문자열에 포함되도록 만들어져있음


7. 요약

  • runBlocking, launch 함수 → 코루틴 빌더 함수의 일종
  • launch 함수를 호출하면 Job 객체가 만들어져 반환되며, Job 객체는 코루틴의 상태를 추적하고 제어하는데 사용됨
  • Job 객체의 Join 함수를 호출하면 함수를 호출한 코루틴이 Job 객체의 실행이 완료될 때까지 일시 중단됨
  • JoinAll 함수를 통해 여러개의 코루틴이 실행 완료될 때까지 대기할 수 있음
  • Job 객체의 cancel 함수를 사용해 코루틴의 취소를 요청할 수 있음
  • cancel 함수를 호출한다고 코루틴이 즉시 취소되는 것이 아닌 코루틴의 취소 플래그의 상태가 바뀌고, 취소가 확인될 때 취소됨
  • cancel 함수를 호출하더라도 코루틴이 취소를 확인할 수 없는 상태에서는 계속해서 실행될 수 없음
  • delay, yield 함수와 isActive 프로퍼티 등을 호라용해 코루틴이 취소를 확인할 수 있도록 만들 수 있음
  • 코루틴은 생성, 실행 중, 실행 완료 중, 실행 완료, 취소 중, 취소 완료의 상태를 가짐
  • Job 객체는 isActive, isCancelled, isCompleted 프로퍼티를 통해 코루틴의 상태를 나타냄
  • isActive는 생성 상태일 때는 false, 코루틴이 실행되면 true로 바뀜, cancel 함수를 통해 취소 요청 또는 실행 완료시 false로 다시 바뀜
  • isCancelled는 코루틴이 취소 중이거나 취소 완료되었을 때만 true가 됨
  • isCompleted는 코루틴이 취소 완료되었거나 실행 완료되었을 때만 true가 됨

0개의 댓글