Coroutine(Builders) - Chapter2

HEETAE HEO·2022년 7월 13일
0
post-thumbnail

코루틴 빌더(Coroutine Builders)

두 번째 챕터는 Coroutine Builders에서 launch, runBlocking and async에 대해서
글을 작성해보도록 하겠습니다.

코루틴 빌더에는 3가지의 함수가 있습니다.

  1. launch

  2. async

  3. runBlocking

입니다. 이들은 코루틴을 생성하는 방법들입니다. 그러나 이것들 보다 중요한 것이 있는데 바로
launch의 경우 GlobalScope.launch라고 부르기도 합니다. GlobalScope는 기본적으로 companion object입니다.

companion object란?
일단 object에 대해 먼저 설명해 드리겠습니다. object는 흔히 JAVA에서 사용하는 무명 내부 클래스 처럼 사용할 수 있습니다.
object 키워드는 클래스를 정의하면서 객체를 생성하는 키워드 라고 생각하시면 됩니다.
그렇다면
companion object : Package수준의 최상위 함수와 객체 선언을 사용할 수 있도록 해주는 기능을 수행합니다.

launch

저번 시간에 GlobalScope.launch{}를 통해서 코루틴을 동작시켜보았습니다. async도 마찬가지입니다. GlobalScope.async{}를 생성할 수 있습니다. runBlocking의 경우는 그냥 runBlocking 그대로 사용합니다.

다음과 같이 Login Screen이 있고 Home Screen이 있습니다. Login Screen에는 launch를 통해 c1이라는 코루틴이 동작하고 있고 Home Screen에서도 launch를 통해 c2라는 코루틴이 동작을 하게됩니다.

이때 Home Screen에서 back 버튼을 눌러 Login 화면으로 되돌아 간다면 Home Screen은 Destroy 되고 c2또한 Destroy됩니다. 왜 그럴까요? 바로 launch 함수는 코루틴을 local scope안에 생성하기 때문입니다. 그 local scope는 생성된 곳이기 때문에 Home Screen이 종료되면 Home Screen에 생성된 c2 코루틴 또한 종료가 되는 것입니다.

그런데 GlobalScope.launch{} 를 통해 코루틴을 생성하게 된다면 이 코루틴은 global level 즉 최상위 scope를 가지기 때문에 어플리케이션이 종료가 되지않는다면 코루틴은 제거 되지 않습니다.

//creates coroutine at global (app) level

GlobalScope.launch {
	// File download
    // play music
}

다음과 같이 앱을 사용함에 있어 부분적인 기능을 GlobalScope에서 launch하는 것이 아닌 앱 전체에서 사용되는 기능을 다음과 같이 Global에다가 launch합니다. Global Coroutine의 경우 어플리케이션이 종료되기 전까지 동작하기에 사용할 때는 주의해서 사용하여야합니다.

//creates coroutine at local scope
launch{
	// some data computation
    // Login operation
}

일반적인 launch에는 부분적인 기능들을 넣어 다음과 같이 동작을 하게 해줍니다.

간단한 코드를 더 설명해드리겠습니다.

fun main() = runBlocking { // Creates a blocking coroutine that executes in current thread(main)

	println("Main program starts : ${Thread.currentThread().name}") // main
    
    launch{
    	println( "Fake work starts : ${Thread.currentThread().name}")
        delay(1000)
		println( "Fake work finished : ${Thread.currentThread().name}")
		}
    delay(2000)
    
    println("Main program ends : ${Thread.currentThread().name}") 
}

다음과 같은 코드를 실행한다면 결과는 다음과 같습니다.

Main program starts : main
Fake work starts : main
Fake work finished : main
Main program ends : main

코루틴으로 실행한 부분이 Main Thread에 의해 실행되었습니다. 왜 이런 결과가 나왔을까요?
그 이유는 launch 코루틴 빌더는 로컬 Scope의 안에 생성되기 때문입니다. 여기서 scope는 누구일 까요? 바로 runBlocking입니다.

runBlocking은 main Thread에서 동작을 하기에 하위 코루틴들이 main Thread에서 생성이 되어 동작되는 것입니다. 만약 runBlocking이 T1이라는 스레드에서 동작을 했다면 결과는 T1이 나왔을 것입니다. 그렇기에 launch를 사용할 때는 이러한 부분을 생각하고 작성해야합니다.

public fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job {
    val newContext = newCoroutineContext(context)
    val coroutine = if (start.isLazy)
        LazyStandaloneCoroutine(newContext, block) else
        StandaloneCoroutine(newContext, active = true)
    coroutine.start(start, coroutine, block)
    return coroutine
}

이 코드들은 launch에 대한 Coroutine 내부 코드입니다.
launch는 Job이라는 객체를 반환하는 동작을 하고 있습니다. 한번 알아보겠습니다.

fun main() = runBlocking { // Creates a blocking coroutine that executes in current thread(main)

	println("Main program starts : ${Thread.currentThread().name}") // main
    
    val job : Job = launch{
    	println( "Fake work starts : ${Thread.currentThread().name}")
        delay(1000)
		println( "Fake work finished : ${Thread.currentThread().name}")
		}
    
    job.join()
    
    println("Main program ends : ${Thread.currentThread().name}") 
}

launch앞에 job을 만들어줘 사용하게 된다면 코루틴을 제어할 수 있습니다. delay()를 통해 코루틴 내부 동작이 끝날때 까지 대기하였지만 delay() 대신에 job.join()을 사용한다면 코루틴의 동작이 종료될때 까지 대기하게 됩니다. 그렇기에 결과는 다음과 같이 나오게됩니다.

Main program starts : main
Fake work starts : main
Fake work finished : main
Main program ends : main

이렇게 사용하게 된다면 코루틴 내부 동작 처리에 대한 시간을 신경쓸 필요없이 job.join()을 통해 사용자가 사용하는 환경에 따라 달라질 수 있는 시간을 적절하게 처리하게 됩니다.

그리고 job을 사용하게 되면 코루틴을 취소할수도 있게됩니다.
취소에 대한 부분은 다음 챕터에서 알려드리도록 하겠습니다.

그럼 이제 다시 launch에 대해서 요약하자면

val job : Job = launch { //c1
	delay(1000) // do some work

launch안에서 delay를 하게된다면 blocking되는 것이 아닌 free가 되는 것이고 이때 다른 suspend 함수의 처리가 가능합니다.

launch coroutine 빌더는(Fire and Forgot)이라고 설명할 수 있고

  • Launches a new coroutine without blocking the current thread
    ->inherits the thread & coroutine scope of the immediate parent coroutine
  • Returns a reference to the Job object
  • Using Job object you can cancel the coroutine or wait for coroutine to finish

다음과 같은 특징들이 있습니다.

async

fun main() = runBlocking { // Creates a blocking coroutine that executes in current thread(main)

	println("Main program starts : ${Thread.currentThread().name}") // main
    
    val job : Job = async{
    	println( "Fake work starts : ${Thread.currentThread().name}")
        delay(1000)
		println( "Fake work finished : ${Thread.currentThread().name}")
		}
    
    job.join()
    
    println("Main program ends : ${Thread.currentThread().name}") 
}

코드에서 launch대신에 async를 사용하게 된다면 결과는 다음과 같습니다.

Main program starts : main
Fake work starts : main
Fake work finished : main
Main program ends : main

launch를 사용해서 얻은 결과 값과 같습니다.
그러나 async를 사용하기에 있는 차이가 있습니다. 바로 Job 객체를 return하지 않습니다. async는 Deferred라는 것을 반환합니다. 이 Deferred는 제네틱 타입을 필요로 합니다.

fun main() = runBlocking { // Creates a blocking coroutine that executes in current thread(main)

	println("Main program starts : ${Thread.currentThread().name}") // main
    
    val jobDeferred : Deferred<String> = GlobalScope.async{
    	println( "Fake work starts : ${Thread.currentThread().name}")
        delay(1000)
		println( "Fake work finished : ${Thread.currentThread().name}")
        "HeeTae" ^async
        
		}
    
    val name : String = jobDeferred.await()
    
    println("Main program ends : ${Thread.currentThread().name}") 
}

제네릭 타입에 String을 넣고 String 값인 "HeeTae"를 넣어주어야합니다. 그리고 await()의 경우 launch의 join과 기능은 같습니다. 바로 기다려주는 것 입니다. async를 GlobalScope에서 동작을 하게 되면 다음과 같은 결과를 주게 됩니다.

Main program starts : main
Fake work starts : DefaultDispatcher-worker-1
Fake work finished : DefaultDispatcher-worker-1
Main program ends : main

Deferred의 내부 코드를 보면 다음과 같이 선언되어있습니다.

public interface Deferred<out T> : Job {


}

이 클래스는 사실 Job의 subclass인 것입니다. 그렇기에 async에서도 대기와 취소 기능을 사용할 수 있습니다.

async 코루틴 빌더를 요약하자면

  • Launches a new coroutine without blocking the current thread
    -> inherits the thread & coroutine scope of the immediate parent coroutine
  • Returns a reference to the Deferred<T.> object
  • Using Deferred object you can cancel the coroutine, wait for coroutine to finish or retrieve the returned result

마지막으로 runBlocking을 보겠습니다.

runBlocking

fun main() = runBlocking { // Creates a blocking coroutine that executes in current thread(main)

	println("Main program starts : ${Thread.currentThread().name}") // main
    
    val jobDeferred : Deferred<String> = GlobalScope.async{
    	println( "Fake work starts : ${Thread.currentThread().name}")
        delay(1000)
		println( "Fake work finished : ${Thread.currentThread().name}")
        "HeeTae" ^async
        
		}
    
    val name : String = jobDeferred.await()
    
    println("Main program ends : ${Thread.currentThread().name}") 
}

현재 이 코드에서는 runBlocking을 통해 코루틴들이 동작할 수 있는 scope를 만들어주는데 이는 사실 올바른 사용법이 아닙니다. 코드의 동작을 통해 이해를 돕기위해 이렇게 사용을 한 것입니다.

일반적으로 runBlocking은 테스트 케이스에서 suspending functions을 테스트 하는데 많이 사용됩니다. 예를 들어 다음과 같은 함수가 있습니다.

suspend fun myOwnSuspendingFunc() {
	delay(1000) // do some work
}

테스트 클래스

// 테스트 클래스 작성

class SimpleTest{

	//이렇게 작성한다면 5+5의 값이 10인지 확인하는 테스트 코드가 완성됩니다. 
    //myOwnSuspendingFunc를 불러올때 suspend 함수는 코루틴 내부 또는 다른 suspend 함수 내부에서만 호출해서 사용할 수 있습니다. 
	// 그렇기에 myFirstTest에 runBlocking을 사용하는 것입니다.
	@Test
    fun myFirstTest() = runBlocking {
    	myOwnSuspendingFunc()
    	Assert.assertEquals(10, 5 + 5)
    }
}

이렇게 Coroutine builders에 대해 정리를 마치고 다음 챕터로 돌아오겠습니다.

읽어주셔서 감사합니다.

profile
Android 개발 잘하고 싶어요!!!

0개의 댓글