[Coroutine] 코루틴 스코프

업데이트:

코루틴 스코프란?

코루틴은 코루틴 스코프 안에서만 동작한다. 특히 일시 중단 함수 즉 await(), join(), delay(), suspend 메소드는 코루틴 스코프 안에서만 호출이 가능하다. 위 api를 코루틴 스코프가 아닌 다른 영역에서 사용하면 컴파일 에러가 발생한다.

위 코루틴 스코프를 시용하기 위해 미리 정의된 코루틴 스코프를 사용하거나 직접 생성하는 방법이 있다.

GlobalScope

GlobalScope는 CoroutineScope의 한 종류로써 가장 큰 특징은 Application이 시작하고 종료될 때까지 계속 유지가 된다. Singletone 이기 때문에 따로 생성하지 않아도 되며 어디에서든 바로 접근이 가능하여 간단하게 사용하기 쉽다는 장점이 있다.

단점으로는 GlobalScope를 사용하면 메모리 누수의 원인이 될 수 있기 때문에 신중히 사용해야 한다. 즉 다시 말해 앱이 실행된 이 후 계속 수행이 되어야 한다면 GlobalScope 를 사용해야 하는 것이고 특정 Activity나 Service 에서만 잠깐 사용하는 것이라면 GlobalScope를 사용하면 안된다.

보통 GlobalScope.launch 를 사용하여 코루틴을 실행한다.

fun main() {

    GlobalScope.launch {
        //run coroutine

    }
}

runBlocking

runBlocking 에 넘겨준 코루틴 로직을 모두 수행 할 때까지 대기를 하게 된다.

아래 코드를 실행하면

fun main() {
    
    runBlocking {
        delay(5000)
        println("step 1")
    }
    println("step 2")
}

Step 1 > Step 2 가 실행됨을 항상 보장한다. 왜냐하면 runBlocking의 코루틴이 모두 실행될 때까지 메인 쓰레드가 대기하기 때문이다.

아래와 같이 runBlocking 대신 GlobalScope.launch 로 바꾸면

fun main() {

    GlobalScope.launch {
        delay(5000)
        println("step 1")
    }
    println("step 2")
}

Step 2 만 출력이 된다. 왜나하면 GlobalScope.launch 는 메인쓰레드를 블록킹하지 않으며 코루틴과 main() 로직을 병렬적으로 실행이 된다. 다만 코루틴이 종료되기 전에 main() 이 먼저 종료되기 때문에 step1이 출력되지 않는다.

runBlocking() 같은 경우 코루틴의 장점인 병렬/비동기 처리에 대한 장점을 활용하지 못하며 특히 Android 메인 스레드에서 호출하면 ANR를 발생할 수 있기 때문에 일반 비즈니스 로직에 사용되지 않으며 주로 Unit test 에 사용한다.

withContext

현재 실행되고 있는 코루틴의 컨텍스트를 변경할 때 사용된다. 가장 대표적인 예는 Android 기준으로 http request를 수행하고 그 결과를 화면에 표시한다고 가정하면 아래와 같은 코드가 될 것이다.


GlobalScope.launch(IO) {
    val result = fetch(httpUrl)

    withContext(Main) {
        resultTextView.text = result.toString()
    }

}

참고로 안드로이드 메인 쓰레드에서는 HTTP request 동작을 수행할 수 없으며 UI 변경(TextView 변경) 은 반드시 메인 쓰레드에서만 수행해야 하기 때문에 컨텍스트 스위칭(쓰레드 스위칭)은 반드시 수행이 되어야 한다.

위 코루틴을 사용하지 않는다면 가장 고전적인 방법으로 핸들러를 사용하여 메인 쓰레드에 메시지를 전송해야 하는 번거로움을 감수해야 한다.

CoroutineScope

위에서 미리 정의된 코루틴 스코프를 사용하는 것이 심플하고 편하지만 제일 이상적인 것은 필요할 때 생성하고 필요하지 않을 때 정리를 하는 것이다.

코루틴 스코프는 아래와 같이 생성하고 정리를 하면 된다.

val scope = CoroutineScope(Dispatchers.Main)

val job = scope.launch {
    // run..
}

job.cancel()

보통 Android Activity에서는 다음과 같이 정의하여 코루틴 스코프와 Activity 의 생명 주기와 일치한다.

class SearchActivity() : CoroutineScope { // 코루틴 스코프를 상속

    private lateinit var job: Job
    override val coroutineContext: CoroutineContext // 코루틴 컨텍스트를 재정의
        get() = Main + job

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        job = Job()
        ...
    }

    fun doOnBackground() {
        launch(IO) {
            // network, file I/O, DB operation
            withContext(Main) {
                // Update UI
            }
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
}

위 코드는 Activity가 destory가 되면 백그라운드로 실행 중인 코루틴이 job.cancel() 를 통해서 정리가 됨으로써 필요할 때 생성하고 필요하지 않을 때 정리를 하는 원칙 을 준수하게 된다.

Reference

코틀린 동시성 프로그래밍(에이콘)

댓글남기기