본문 바로가기
도서/코틀린 코루틴

2장 코루틴 라이브러리 - Coroutine Buidler

by 안스 인민군 2024. 2. 3.

개요

  • 모든 중단 함수에서 일반 함수를 호출하는 것은 문제가 없지만, 일반 함수에서 중단 함수를 호출할 수는 없다.
  • 모든 중단 함수는 다른 중단함수에서 호출이 되어야 한다.
  • 중단 함수는 어떻게 시작할 수 있을까? 바로 Coroutine Builder를 통해 시작할 수 있다.

launch builder

launch builder 가 동작하는 방식은 개념적으로 thread 함수를 사용해서 thread 를 시작하는 것 과 비슷하다.

독립적으로 코루틴이 실행이 되며 아래 예제와 같이 사용할 수 있다.

fun main() {
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    Thread.sleep(2000L)
}
// Hello,
// (1 초 후)
// World!
// World!
// World!

위의 예시로 확인 할 수 있는 사항은..

  1. launch 빌더는 CoroutineScope 의 확장 함수이다.
  2. delay 함수는 Thread를 black 하지 않지만 Thread.sleep은 Thread를 block 한다. 이유는 이전 장에서 본 것처럼 deley 내부 함수는 코루틴을 통해 다른 Thread에서  Thread.sleep을 사용하기 때문
  3. launch 함수의 반환값은 Job 이다. 해당 코루틴을 함수에서 반환한다면 외부에서 코루틴을 취소하거나 재개할 수 있다. (join, cancel)

runBlocking builder

위의 launch builder에서는 코루틴이 thread 를 block 하지 않아, 별도의 중단 함수를 추가했다.

runBlocking builder는 별도의 중단함수 없이, thread 를 block 할 수 있다.

 

runBlocking Builder으로 코루틴이 생성될 경우, 해당 코루틴이 완료될 때 까지 현재 스레드를 block 한다. 따라서 runBlocking 내부에서 delay 함수를 호출 할 경우 Thead.sleep 함수와 비슷하게 작동한다.

fun main(args: Array<String>) = runBlocking {
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    delay(2000L)
}

runBlokcing이 사용되는 몇가지 사례이다.

  1. 프로그램이 끝나는 걸 방지하기 위해 thread 를 block 하고 싶을때
  2. 위의 사유로, Unit Test 를 사용할 때
fun main() = runBlocking {
    // ...
}
class MyTest {    @Test
    fun `a test`() = runBlocking {
        // ...
    }
}
fun main() = runBlocking {
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    delay(2000L) // 필요함
}

runBlocking builder 은 중요하게 사용되었지만, 현재는 거의 사용되지 않는다.

유닛 테스트에서 가상 시간으로 실행시키는 runTest가 runBlocking 을 대체해서 사용되고 있다.

메인 함수에서는 runBlocking 대신에 suspend 를 붙여 중단 함수로 만들어 사용 할 수 있다.

suspend fun main() {
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    delay(2000L)
}

async builder

async builder는 lauch buidler와 유사하지만, 다른점은 값을 직접 생성한다.

이 값은 lamda 표현식을 통해 반환이 되어야 한다.

await함수는 Deferred<T> 타입의 객체가 반환되며 T 는 생성되는 값의 타입이다.

Deferred에는 작업이 끝나면 값을 반환하는 await 중단 메서드가 있다.

아래 예시를 보면 값이 42가 생성되었고, 타입은 Int 이므로 Deferred<Int> 가 반환된다.

그리고 Deferred<Int>의 await을 통해 42가 반환된다.

fun main = runBlocking {
    val resultDeferred: Deferred<Int> = GlobalScope.async {
        delay(1000L)
        42
    } 
    val result: Int = resultDeferred.await() // (1 초)
    println(result) // 42
    // 간단하계 작성 가능
    println(resultDeferred.await()) // 42
}

async builderlaunch builder 와 유사하게 호출하는 시점에서 코루틴을 시작한다.

따라서 몇 개의 작업을 한 번에 시작하고, 모든 결과를 기달릴 때 사용된다.

값이 생성되면 Deferred 내부에 저장하기 때문에 바로 사용할 수 있다.
하지만 값이 생성되기 전 await 함수를 호출되는 경우, 값이 준이 준비될 때 까지 기다리게 된다.

fun main = runBlocking {
    val res1 = GlobalScope.async {
        delay(1000L)
        "Text 1"
    }
    val res2 = GlobalScope.async {
        delay(3000L)
        "Text 3"
    }
    val res3 = GlobalScope.async {
        delay(2000L)
        "Text 2"
    }
    println(res1.await())
    println(res2.await())
    println(res3.await())
}
// (1 초)
// Text 1
// (2 초)
// Text 2
// Text 3

async 호출 시점에 코루틴을 시작하므로 3초 이후에 모든 값이 반환된다.

async builder는 launch builder 와 유사하게 동작하지만 값을 반환하는 특징을 가지고 있다.

그 말은 즉슨 lauch builder 은 async builder 로 모두 교체할 수 있지만 용도에 맞게 사용해야 한다.

 

async builde는 두 가지 데이터를 병렬적으로 가져와 합쳐야 하는 상황일 때 사용할 수 있다.

scope.launch {
    val news = async {
        newsRepo.getNews().sortedByDescending { it.date }
    } 
    val newsSummary = newsRepo.getNewsSummary()    

    views.showNews(
        newsSummary,
        news.await()
    )
}

구조화된 동시성

만약 코루틴이 GolbalScope에서 시작되었다면 프로그램은 해당 코루틴을 기디랄 시 없다.

코루틴은 어떠한 thread 도 block 하지 않기 때문이다. 따라서 프로그램이 종료되는 것을 막을 수 없다.

이 이유로 인해 아래의 예제에서 delay 함수를 호출해야 한다.

fun main() = runBlocking {
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }    
    GlobalScope.launch {
        delay(2000L)
        println("World!")
    }
    println("Hello,")
    // delay(3000L)
}
// Hello,
(1초뒤)
// World!
(2초뒤)
// World!

Global Scope 를 왜 앞에 붙이는 것일까? 바로 launch 와 async 함수는 CoroutineScope의 확장함수이기 때문이다.

한번 내부 코드를 확인해 보자.

runBlocking

public actual fun <T> runBlocking(
      context: CoroutineContext,
      block: suspend CoroutineScope.() -> T
): T

launch

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

async

public fun <T> CoroutineScope.async(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> T
): Deferred<T>

runBlokcing 구현을 보면 마지막 block 파라미터가 CoroutineScope가 리시버 타입인 CoroutineScope 함수 타입인 것을 알 수 있다.

이 는 별도의 CoroutineScope 을 사용하지 않고, 해당 리시버를 통해 launch, async를 호출할 수 있다.

위의 내용을 부모와 자식관계로 풀어내면

runBlocking(부모) - launch(자식) 이고 parentchild 코루틴을 기다려야 하는 의무가 있기 때문에 child이 모든 작업을 끝마칠때까지 runBlocking은 중단(suspend) 된다.

fun main() = runBlocking {
    this.launch { // == launch
        delay(1000L)
        println("World!")
    } 
       launch { // == this.launch
        delay(2000L)
        println("World!")
    }
    println("Hello,")
}
// Hello,
// (1 초)
// World!
// (1 초)
// World!

parent는 children들을 위한 scope를 제공하고,해당 scope 내에서 호출하게 된다.

이를 통홰 구조화된 동시성(Structured Concurrency)이라는 관계가 성립된다.

parent - child 관계의 가장 중요한 특징들 이다.

  • child는 parent 의 context 를 상속받는다. (child 은 이를 override 가능합니다.)
  • parent 는 모든 children 이 끝마칠 때 까지 기다린다.
  • parent coroutine 이 cancel 되면 child coroutine 도 cancel 된다. (반대는 적용되지 않음)
  • child coroutine 에서 error 가 발생하면 parent coroutine 또한 error 로 소멸한다. (반대에서도 똑같이 발생함)

위에서 본 것 처럼 coroutine builder와 다르게 runBlocking builderCoroutineScope 의 확장함수가 아니다.

runBlocking은 child 가 될 수 없으며, parent coroutine 만이 될 수 있습니다. 따라서 runBlocking은 다른 코루틴과 쓰임새가 많이 다르게 사용된다.