본문 바로가기
Programando/Android

[Android/Kotlin] Coroutines

Coroutines은 비동기적으로 실행되는 코드를 간소화하기 위해 안드로이드에서 사용할 수 있는 동시 실행 설계 패턴입니다.

 

비동기(Asynchronous) 처리

비동기 처리에서는 병렬적으로 태스크를 수행합니다. 만약 태스크가 종료되지 않은 상태라 하더라도 결과가 나올 때까지 대기하지 않고, 다음 태스크를 실행하게 됩니다.

Coroutines은 서브 루틴을 일시 정지하고 재개할 수 있는 구성 요소를 말합니다. 실행 중인 스레드를 차단하지 않고, 일시 정지를 지원하므로 단일 스레드에서 많은 코루틴을 실행할 수 있습니다. 또, 개발자가 직접 루틴을 언제 실행할지, 언제 종료할지를 지정할 수 있습니다. 이렇게 생성한 루틴은 작업 전환 시에 시스템의 영향을 받지 않기 때문에 그에 따른 비용이 발생하지 않습니다.

 

Coroutines 기본적인 사용법

0. 종속성 추가

Coroutines을 사용하기 위해서는 먼저 app 모듈의 build.gradle에 종속성을 추가해야 합니다.

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

 

1. Coroutines Scope

Scope는 그 Scope에서 생성된 Coroutines을 계속해서 주시하면서 실행을 취소하거나, 실패 시 예외를 처리할 수 있도록 합니다. Scope는 커스텀하거나 또는 이미 내장된 범위를 사용할 수 있습니다.

ScopeGlobalScopeCoroutineScope가 존재하며, CoroutineScope의 경우에는 Dispatcher를 지정할 수 있습니다.

class MainActivity : AppCompatActivity() {

    private val tvCoroutineValue : TextView by lazy {
        findViewById(R.id.tvCoroutineValue)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch {

            tvCoroutineValue.text = "Coroutine을 통한 값 넣기 (GlobalScope)"

        }

        CoroutineScope(Dispatchers.Main).async {

            tvCoroutineValue.text = "Coroutine을 통해 값 넣기 (CoroutineScope)"

        }

    }
}

 

2. Coroutines Dispatcher 지정

DispatcherCoroutines을 적당한 스레드에 할당하며, Coroutines 실행 도중 일시 정지나 실행 재개를 담당합니다.

Default, IO, Main, Unconfined 등이 있습니다.

  • Default안드로이드 기본 스레드풀을 사용하여 CPU를 많이 사용하는 작업에 최적화되어 있습니다. 보통 데이터 정렬이나 복잡한 연산을 할 때 지정합니다.
  • IO는 이미지 다운로드나 파일 입출력 등 입출력에 최적화되어 있습니다. 보통 네트워크나 디스크, DB 작업을 할 때 지정합니다.
  • Main안드로이드 기본 스레드에서 실행합니다. 보통 UI나 스레드를 막지 않고 빨리 실행되는 작업을 할 때 지정합니다.
  • Unconfined는 특정 스레드 또는 특정 스레드풀을 지정하지 않습니다. 일반적으로는 사용하지 않으며 특정 목적을 위해서만 사용됩니다.
class MainActivity : AppCompatActivity() {

    private val tvCoroutineValue : TextView by lazy {
        findViewById(R.id.tvCoroutineValue)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch {

            tvCoroutineValue.text = "Coroutine을 통한 값 넣기 (GlobalScope)"

        }

        CoroutineScope(Dispatchers.Main).async {

            tvCoroutineValue.text = "Coroutine을 통해 값 넣기 (CoroutineScope)"

        }

    }
}

 

2. Coroutines 시작

Coroutineslaunchasync 두 가지 방법 중 하나로 시작할 수 있습니다.

  • launch는 현재 스레드를 차단하지 않고, 새로운 Coroutines를 실행하고, Coroutines을 제거할 때 사용할 수 있습니다. ‘실행 후 삭제’로 간주되는 모든 작업은 launch를 사용하여 시작할 수 있습니다. launchJob 객체를 반환하며, 이 Joblaunch로 생성된 Coroutines의 상태를 관리하는 용도로 사용하고, 결과를 반환받을 수 없습니다.
  • async는 새로운 Coroutines를 시작하고,await라는 정지 함수를 통해 결과를 반환할 수 있게 허용합니다. asyncDeferred<T> 객체를 반환하며, 이 Deferred<T>async 블록 내 수행된 결과를 원하는 시점에 반환받을 수 있습니다.
  • DeferredJob을 상속받아 구현되었기 때문에 Job의 기능을 사용할 수 있습니다.
class MainActivity : AppCompatActivity() {

    private val tvCoroutineValue: TextView by lazy {
        findViewById(R.id.tvCoroutineValue)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        GlobalScope.launch {

            tvCoroutineValue.text = "Coroutine을 통한 값 넣기 (GlobalScope)"

        }

        CoroutineScope(Dispatchers.Main).async {

            tvCoroutineValue.text = "Coroutine을 통해 값 넣기 (CoroutineScope)"

        }

    }
}

 

반응형

 

Coroutines Method

cancel

Coroutines의 동작을 멈추는 상태관리 메서드로 하나의 Scope 안에 여러 Coroutines이 존재하는 경우 하위 Coroutines 또한 모두 멈춥니다.

class MainActivity : AppCompatActivity() {

    private val tvCoroutineValue: TextView by lazy {
        findViewById(R.id.tvCoroutineValue)
    }

    private val btnCoroutineCancel: Button by lazy {
        findViewById(R.id.btnCoroutineCancel)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val job = CoroutineScope(Dispatchers.Default).launch {

            for(i in 0..10) {
                delay(1000)
                Log.d("Coroutines", "$i")
            }
        }

        btnCoroutineCancel.setOnClickListener {

            job.cancel()

        }

    }

}

 

join

Coroutines 내부에 여러 launch 블록이 있는 경우, 모두 새로운 Coroutines으로 분기되어 동시에 실행되기 때문에 순서를 정할 수 없습니다. 하지만 순서를 정해야 한다면 join 메서드를 통해 순차적으로 실행되도록 코드를 짤 수 있습니다.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        CoroutineScope(Dispatchers.Default).launch {

            launch {
                for(i in 0..5) {
                    delay(1000)
                    Log.d("Coroutines", "i : $i")
                }
            }.join()

            launch {
                for(j in 6..10) {
                    delay(1000)
                    Log.d("Coroutines", "j : $j")
                }
            }

        }

    }

}

 

async로 결과값 처리

asyncCoroutines Scope의 결과를 받아서 쓸 수 있습니다. 특히, 연산 시간이 오래 걸리는 2개의 네트워크 작업의 경우에는 2개의 작업이 모두 완료되고 나서 이를 처리하기 위해 await을 사용할 수 있습니다. 이때는 async 작업이 모두 완료되고 나서야 await 코드가 실행됩니다.

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        CoroutineScope(Dispatchers.Default).async {

            val deferred1 = async {
                delay(1000)
                600
            }

            val deferred2 = async {
                delay(1500)
                200
            }

            Log.d("Coroutines", "${deferred1.await()}, ${deferred2.await()}")

        }

    }

}

 

suspend

Coroutines 안에서 suspend 함수가 호출될 경우 이전까지의 코드 실행이 멈추며, suspend 함수의 처리가 완료된 후 멈춰 있던 원래 Scope의 다음 코드가 실행됩니다.

 

withContext

호출 쪽 CoroutinesDispatchers.Main으로 UI를 제어하고, suspend 함수에서 네트워크나 DB 작업 등을 하는 경우에는 withContext를 사용하여 suspend 함수의 DispatcherIO로 변경하여 사용할 수 있습니다.

class MainActivity : AppCompatActivity() {

    private val tvCoroutineValue: TextView by lazy {
        findViewById(R.id.tvCoroutineValue)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        CoroutineScope(**Dispatchers.Main**).launch {

            tvCoroutineValue.text = "Coroutines를 통한 값 넣기(CoroutineScope)"

            withContext(Dispatchers.IO) {
                // 네트워크, DB 작업 등
            }

        }

    }

}

 

Job

launch, async와 같은 빌더를 호출하면 Job 객체가 반환됩니다. JobCoroutines의 생명주기를 관리할 수 있고, 내부에서 다시 빌더를 호출하면 자식 Job이 생성됩니다. 부모 Job 취소 시에는 자식 Job도 취소되며, 반대의 경우에는 취소되지 않습니다.

하지만 launch 빌더로 생성한 자식 Job은 예외 발생 시에 부모 Job을 취소시킵니다. async 빌더로 생성한 자식 Job은 예외가 발생해도 부모 Job이 취소되지 않는데 이는 반환 결과에 예외도 포함시키기 때문입니다.

 

그 외

전체 코드

https://github.com/na-ram/AOS_Coroutines.git

 

자료 참조

https://whyprogrammer.tistory.com/596

반응형