반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[코루틴의 정석] 4장. 코루틴 빌더와 Job 본문

Kotlin/코루틴의 정석

[코루틴의 정석] 4장. 코루틴 빌더와 Job

어리둥절범고래 2024. 8. 24. 01:32
반응형

코루틴 빌더는 코루틴을 생성하는 데 사용되는 함수이다.

 

  • 모든 코루틴 빌더 함수는 코루틴을 만들고 코루틴을 추상화한 Job 객체를 생성한다.
  • Job 객체는 코루틴의 상태를 추적하고 제어하는데 사용된다.
  • 코루틴은 일시 중단할 수 있는 작업으로 실행 도중 일시 중단된 후  나중에 이어서 실행될 수 있다.
  • Job 객체는 코루틴을 제어할 수 있는 함수와 상태 값을 외부에 노출한다.

 

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

 

Job 객체는 join 함수를 제공해 먼저 처리돼야 하는 코루틴의 실행이 완료될 때까지 호출부의 코루틴을 일시 중단하도록 만들 수 있다.

 

4.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-1 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-3 @coroutine#3] 네트워크 요청
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 완료
*/

이 코드에서는 인증 토큰 업데이트 작업과 네트워크 요청 작업이 병렬로 동시에 실행된다.

updateTokenJob이 모두 완료된 이후에 networkCallJob이 실행돼야 한다.

 

4.1.2. join 함수 사용해 순차 처리하기


Job 객체의 join 함수를 사용하면 코루틴 간에 순차 처리가 가능하다.

fun main() = runBlocking<Unit> {
  val updateTokenJob = launch(Dispatchers.IO) {
    println("[${Thread.currentThread().name}] 토큰 업데이트 시작")
    delay(100L)
    println("[${Thread.currentThread().name}] 토큰 업데이트 완료")
  }
  updateTokenJob.join() // updateTokenJob이 완료될 때까지 일시 중단
  val networkCallJob = launch(Dispatchers.IO) {
    println("[${Thread.currentThread().name}] 네트워크 요청")
  }
}
/*
// 결과:
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 완료
[DefaultDispatcher-worker-1 @coroutine#3] 네트워크 요청
*/

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()
  val networkCallJob = launch(Dispatchers.IO) {
    println("[${Thread.currentThread().name}] 네트워크 요청")
  }
}
/*
// 결과:
[DefaultDispatcher-worker-1 @coroutine#2] 토큰 업데이트 시작
[DefaultDispatcher-worker-3 @coroutine#3] 독립적인 작업 실행
[DefaultDispatcher-worker-1 @coroutine#2] 토큰이 업데이트 됐습니다
[DefaultDispatcher-worker-1 @coroutine#4] 네트워크 요청
*/

 


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

 

코루틴 라이브러리는 복수의 코루틴의 실행이 모두 끝날 때까지 호출부의 코루틴을 일시 중단시키는 joinAll 함수를 제공한다.

 

4.2.1. joinAll 함수

 

joinAll 함수 내부는 다음과 같이 가변 인자로 job 타입의 객체를 받은 후 각 Job 객체에 대해 모두 join 함수를 호출한다.

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

 

4.2.2. joinAll 함수 사용해 보기

 

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) // 이미지1과 이미지2가 변환될 때까지 대기

  val uploadImageJob: Job = launch(Dispatchers.IO) {
    println("[${Thread.currentThread().name}] 이미지1,2 업로드")
  }
}
/*
// 결과:
[DefaultDispatcher-worker-1 @coroutine#2] 이미지1 변환 완료
[DefaultDispatcher-worker-2 @coroutine#3] 이미지2 변환 완료
[DefaultDispatcher-worker-1 @coroutine#4] 이미지1,2 업로드
*/



4.3. CoroutineStart.LAZY 사용해 코루틴 지연 시작하기

 

코루틴 라이브러리는 나중에 실행돼야 할 코루틴을 미리 생성할 수 있는 지연 시작 기능을 제공한다.

 

4.3.1. 지연 시작을 살펴보기 위한 준비

 

지연 시작 기능에 대해 살펴보기 위해 시간을 측정할 수 있는 도구가 필요하다.

fun getElapsedTime(startTime: Long): String =
  "지난 시간: ${System.currentTimeMillis() - startTime}ms"
fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val immediateJob: Job = launch {
    println("[${getElapsedTime(startTime)}] 즉시 실행") // 지난 시간 측정
  }
}
/*
// 결과:
[지난 시간: 2ms] 즉시 실행
*/

블록이 시작되는 시간을 startTime을 통해 기록하며, 원하는 지점에 getElapsedTime 함수의 인자로 startTime을 넘김으로써 지난 시간을 측정할 수 있다.

 

 

4.3.2. CoroutineStart.LAZY 사용해 코루틴 지연 시작하기

 

launch 함수의 start 인자로 CoroutineStart.LAZY를 넘겨 코루틴에 지연 시작 옵션을 적용해야 한다.

이 코루틴은 별도 실행 요청이 있을 때까지 실행되지 않는다.

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val lazyJob: Job = launch(start = CoroutineStart.LAZY) {
    println("[${Thread.currentThread().name}][${getElapsedTime(startTime)}] 지연 실행")
  }
  delay(1000L) // 1초간 대기
  lazyJob.start() // 코루틴 실행
}
/*
// 결과:
[main @coroutine#2][지난 시간: 1014ms] 지연 실행
*/

실행 요청 함수인 start를 하기 전까지 코루틴은 곧바로 실행되지 않고 1초 대기 후 실행된 것을 확인할 수 있다.

 


4.4. 코루틴 취소하기

 

코루틴이 필요가 없어졌음에도 취소하지 않고 계속해서 실행되도록 두면 코루틴은 계속해서 스레드를 사용하며 이는 애플리케이션의 성능 저하로 이어진다.

Job 객체는 코루틴을 취소할 수 있는 cancel 함수를 제공한다.

 

4.4.1. cancel 사용해 Job 취소하기

 

코루틴을 취소하는 방법을 알아보기 위해 오래 실행되는 longJob 코루틴을 만들어 보자.

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val longJob: Job = launch(Dispatchers.Default) {
    repeat(10) { repeatTime ->
      delay(1000L) // 1000밀리초 대기
      println("[${getElapsedTime(startTime)}] 반복횟수 ${repeatTime}")
    }
  }
}
/*
// 결과:
[지난 시간: 1015ms] 반복횟수 0
[지난 시간: 2022ms] 반복횟수 1
[지난 시간: 3028ms] 반복횟수 2
...
[지난 시간: 8050ms] 반복횟수 7
[지난 시간: 9057ms] 반복횟수 8
[지난 시간: 10062ms] 반복횟수 9
*/

 

longJob 코루틴을 취소하기 위해서는 취소를 원하는 시점에 longJob.cancel()을 호출하면 된다

fun main() = runBlocking<Unit> {
  val startTime = System.currentTimeMillis()
  val longJob: Job = launch(Dispatchers.Default) {
    repeat(10) { repeatTime ->
      delay(1000L) // 1000밀리초 대기
      println("[${getElapsedTime(startTime)}] 반복횟수 ${repeatTime}")
    }
  }
  delay(3500L) // 3500밀리초(3.5초)간 대기
  longJob.cancel() // 코루틴 취소
}
/*
// 결과:
[지난 시간: 1016ms] 반복횟수 0
[지난 시간: 2021ms] 반복횟수 1
[지난 시간: 3027ms] 반복횟수 2
*/

 

 

4.4.2. cancelAndJoin을 사용한 순차 처리

 

cancel 함수를 사용하면, Job 객체는 바로 취소되는 것이 아니라 미래의 어느 시점에 취소된다.

cancelAndJoin 함수를 사용하면 취소에 대한 순차 처리가 가능해진다.

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

 



4.5. 코루틴의 취소 확인

 

앞서 설명했듯이 cancel 함수나 cancelAndJoin 함수를 사용했다고 해서 코루틴이 즉시 취소되는 것은 아니다.

이들은 Job 객체 내부에 있는 취소 확인용 플래그를 바꾸기만 하며, 코루틴이 이 플래그를 확인하는 시점에 취소된다.

만약 코루틴이 취소를 확인할 수 있는 시점이 없다면 취소는 일어나지 않는다.

fun main() = runBlocking<Unit> {
  val whileJob: Job = launch(Dispatchers.Default) {
    while(true) {
      println("작업 중")
    }
  }
  delay(100L) // 100밀리초 대기
  whileJob.cancel() // 코루틴 취소
}
/*
// 결과:
...
작업 중
작업 중
작업 중
...
*/

 

4.5.1. delay를 사용한 취소 확인

 

delay 함수는 일시 중단 함수(suspend fun)로 선언돼 특정 시간만큼 호출부의 코루틴을 일시 중단하게 만든다.

코루틴은 일시 중단되는 시점에 코루틴의 취소를 확인하기 때문에 취소를 확인할 수 있다.

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

Process finished with exit code 0
*/


4.5.2. yield를 사용한 취소 확인

 

yield는 '양보'라는 뜻으로, yield 함수가 호출되면 코루틴은 자신이 사용하던 스레드를 양보한다.

yield 함수를 호출하면 호출한 코루틴이 일시 중단되며 이 시점에 취소를 확인할 수 있다.

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

Process finished with exit code 0
*/


4.5.3. CoroutineScope.isActive를 사용한 취소 확인

 

CoroutineScope는 코루틴이 활성화됐는지 확인할 수 있는 Boolean 타입의 프로퍼티인 isActive를 제공한다.

delay와 yield 함수는 일시 중단하기 때문에 작업을 비효율적으로 만들지만, 이 방법은 계속해서 작업을 할 수 있어서 효율적이다.

fun main() = runBlocking<Unit> {
  val whileJob: Job = launch(Dispatchers.Default) {
    while(this.isActive) {
      println("작업 중")
    }
  }
  delay(100L)
  whileJob.cancel()
}
/*
// 결과:
...
작업 중
작업 중
작업 중

Process finished with exit code 0
*/

 

 


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

 

 

코루틴은 위와 같이 6가지 상태를 가진다.

코루틴이 어떤 경우에 각 상태로 전이되는지 살펴보자(실행 완료는 7장에서 다룬다).

 

  • 생성(New): 코루틴 빌더를 통해 코루틴을 생성
  • 실행 중(Active): 코루틴이 실행 중일 때와 실행된 후에 일시 중단된 때도 실행 중 상태
  • 실행 완료(Completed): 코루틴의 모든 코드가 실행 완료
  • 취소 중(Cancelling): Job.cancel() 등을 통해 코루틴에 취소가 요청, 아직 취소된 상태가 아니다.
  • 취소 완료(Cancelled): 코루틴의 취소 확인 시점에 취소가 확인

Job 객체에서 외부로 공개하는 Boolean 타입의 코루틴의 상태 변수는 isActive, isCancelled, isCompleted의 세 가지이다.

 

  • isActive: 코루틴이 활성화되어 있는지의 여부(취소가 요청되거나 실행이 완료되지 않은 상태)
  • isCancelled: 코루틴이 취소 요청되었는지의 여부
  • isCompleted: 코루틴이 실행 완료되었는지의 여부



4.6.1. Job의 상태를 출력하는 함수 만들기

 

Job의 상태를 확인하기 위해 Job의 상태를 출력하는 printJobState 함수다.

fun printJobState(job: Job) {
  println(
    "Job State\n" +
        "isActive >> ${job.isActive}\n" +
        "isCancelled >> ${job.isCancelled}\n" +
        "isCompleted >> ${job.isCompleted} "
  )
}

 


4.6.2. 생성 상태의 코루틴

 

생성 상태는 코루틴이 생성만 되고 실행되지 않은 상태를 말한다.

코루틴 빌더의 start 인자로 CoroutineStart.LAZY를 넘겨 지연 시작이 적용된 코루틴을 생성해야 한다.

fun main() = runBlocking<Unit> {
  val job: Job = launch(start = CoroutineStart.LAZY) { // 생성 상태의 Job 생성
    delay(1000L)
  }
  printJobState(job)
}
/*
// 결과:
Job State
isActive >> false
isCancelled >> false
isCompleted >> false
*/



4.6.3. 실행 중 상태의 코루틴

 

코루틴이 실행되고 있는 상태를 실행 중 상태라고 부른다.

fun main() = runBlocking<Unit> {
  val job: Job = launch { // 실행 중 상태의 코루틴 생성
    delay(1000L)
  }
  printJobState(job) // 코루틴 상태 출력
}
/*
// 결과:
Job State
isActive >> true
isCancelled >> false
isCompleted >> false
*/

 

4.6.4. 실행 완료 상태의 코루틴

 

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

fun main() = runBlocking<Unit> {
  val job: Job = launch {
    delay(1000L) // 1초간 대기
  }
  delay(2000L) // 2초간 대기
  printJobState(job)
}
/*
// 결과:
Job State
isActive >> false
isCancelled >> false
isCompleted >> true
*/

 

4.6.5. 취소 중인 코루틴

 

취소를 확인할 수 있는 시점이 없는 코루틴을 생성하고 취소를 요청하면 취소 중 상태의 코루틴을 확인 할 수 있다.

fun main() = runBlocking<Unit> {
  val whileJob: Job = launch(Dispatchers.Default) { // 취소를 확인할 수 있는 시점이 없는 코루틴 생성
    while(true) {
      // 작업 실행
    }
  }
  whileJob.cancel() // 코루틴 취소 요청
  printJobState(whileJob) // 취소가 요청됐으나 취소가 되지 않은 코루틴의 상태 출력
}
/*
// 결과:
Job State
isActive >> false
isCancelled >> true
isCompleted >> false
*/

 

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

이 때문에 isActive가 false이므로 이를 활용해 취소를 체크한다.

 


4.6.6. 취소 완료된 코루틴

 

취소 요청이 확인되는 시점에 취소가 완료된다.

fun main() = runBlocking<Unit> {
  val job: Job = launch {
    delay(5000L) // 5초간 대기
  }
  job.cancelAndJoin() // 코루틴 취소 요청 + 취소가 완료될 때까지 대기
  printJobState(job) // Job 상태 출력
}
/*
// 결과:
Job State
isActive >> false
isCancelled >> true
isCompleted >> true
*/

취소 요청 후 취소가 완료됐으므로 isCancelled와 isCompleted가 true가 되는 것을 확인할 수 있다.

 



4.6.7. 상태 정리

 

상태 isActive isCompleted isCancelled
New(지연 시작될 때 시작 상태) false false  false
Active(시작 상태 기본값) true false false
Completing(일시적인 상태) true false false
Cancelling(일시적안 상태) false false true
Cancelled(최종 상태) false true true
Completed(최종 상태) false true false

 


요약

 

1. runBlocking 함수와 launch 함수는 코루틴을 만들기 위한 코루틴 빌더 함수 이다.
2. launch 함수를 호출하면 Job 객체가 만들어져 반환되며, Job 객체는 코루틴 의 상태를 추적하고 제어하는 데 사용된다.
3. Job 객체의 join 함수를 호출하면 함수를 호출한 코루틴이 Job 객체의 실행이 완료될 때까지 일시 중단된다.
4. joinAl! 함수를 사용해 복수의 코루틴이 실행 완료될 때까지 대기할 수 있다.
5. Job 객체의 cancel 함수를 사용해 코루틴에 취소를 요청할 수 있다.
6. Job 객체의 cancel 함수가 호출되면 코루틴이 곧바로 취소되는 것이 아니라 코루틴의 취소 플래그의 상태가 바뀌고, 취소가 확인될 때 비로소 취소된다.
7. 코루틴에 취소를 요청한 후 취소가 완료될 때까지 대기하고 나서 다음 코드를 실행하고 싶다면 cancel 대신 cancelAndJoin 함수를 사용하면 된다.
8. cancel 함수를 호출하더라도 코루틴이 취소를 확인할 수 없는 상태에서는 계 속해서 실행될 수 있다.
9. delay, vield 함수나 isActive 프로퍼티 등을 사용해 코루틴이 취소를 확인할 수 있도록 만들 수 있다.
10. 코루틴은 생성, 실행 중, 실행 완료 중, 실행 완료, 취소 중, 취소 완료 상태를 가 진다.
11. Job 객체는 isActive, isCancelled, isCompleted 프로퍼티를 통해 코루틴 의 상태를 나타낸다.
12. isActive는 생성 상태일 때는 false이고 코루틴이 실행되면 true로 바뀌는데 코루틴에 cancel 함수를 통해 취소가 요청되거나 코루틴이 실행 완료되면 다 시 false로 바뀐다.

 

반응형