[Android] Coroutine 사용해보기

지금별·2024년 4월 3일

Coroutine

목록 보기
1/1
post-thumbnail

coroutine의 특성을 알아보았고 이번에는 coroutine을 사용하는 방법에 대해 알아보겠습니다.

coroutine을 사용하기 위해선 시작 방법, 범위, 정보등을 알아야 합니다.
이는 각각 Builder, Scope, Context를 의미하며 자세하게 알아 보도록 하겠습니다.

1. Coroutine Builder

coroutine builder는 coroutine을 시작하는 방법 입니다.
coroutine builder의 종류는 launch, async, runBlocking 등이 있습니다.

  • launch : 새로운 coroutine을 시작하고 결괏값을 반환하지 않습니다.
GlobalScope.launch {
	// 코드
}

  • async : 새로운 coroutine을 시작하고 Deferred<T>를 반환하여 결괏값을 반환받을 수 있습니다.
fun async() = runBlocking {

    val deffered : Deferred<Int> = async{
        var num = 0
        for(i in 0..3){
            num += i
            delay(1000)
        }
        num
    }
    
    val result = deffered.await()
	println("결과 : $result")
}

// 결과 : 6

  • withContext : withContext는 현재 코루틴을 다른 CoroutineContext로 전환할 때 사용됩니다.
    주로 coroutine의 실행 컨텍스트(Dispatcher)를 변경할 때 사용되며, 결과 값을 직접 반환할 수 있습니다.
val result = withContext(Dispatchers.IO) {
    // 다른 컨텍스트에서 실행할 코드
}

  • runBlocking : 현재 스레드를 Blocking 하고 새로운 coroutine을 시작하며 주로 테스트나 메인 함수에서 비동기 작업을 수행합니다.
    runBlocking은 테스트에 사용하는 것을 권장하며 실제 애플리케이션 코드에선 사용하지 않는 것을 추천합니다.
fun main() = runBlocking{
	// 코드
}



2. Coroutine Scope

coroutine scopecoroutine이 실행될 때 그 범위를 정의하는 것을 말합니다.
coroutine scopecoroutine이 어디서 시작되고, 어디서 종료될 것인지, 그리고 어떤 생명 주기에 따라 관리될 것인지를 결정합니다.
즉, coroutine의 실행 범위를 제한하고, coroutine을 안전하게 관리할 수 있습니다.

  • GlobalScope : 애플리케이션 전체에서 실행되는 가장 넓은 범위의 Coroutine Scope입니다.
    수동으로 취소하지 않는 이상 애플리케이션이 종료될 때까지 계속 실행됩니다.
    GlobalScope는 메모리 누수를 유발할 수 있기 때문에 사용하는 것을 권장하지 않습니다.
GlobalScope.launch{
	// 코드
}
  • CoroutineScope : 사용자 정의 scope를 만들 때 사용합니다.
    이는 사용자가 직접 실행 범위를 설정할 수 있습니다. (Ex: Dispatchers)
val myCoroutineScope = CoroutineScope(Dispatchers.Default)
myCoroutineScope.launch{
	// 코드
}
  • lifecycleScopeviewmodelScope : 생명주기에 따라 coroutine을 자동으로 관리할 수 있는 scope입니다.
    lifecycleScopeActivityFragment의 생명주기에 맞춰서 coroutine을 관리할 수 있습니다.
    viewmodelScopeviewmodel의 생명주기에 맞춰 coroutine을 관리할 수 있습니다.
// Activity에서 사용
lifecycleScope.launch{
	// 코드
}

// Fragment에서 사용
viewLifecycleOwner.lifecycleScope.launch{
	// 코드
}

// viewModel에서 사용
viewModelScope.lanuch{
	// 코드
}



3. Coroutine Context

coroutine context는 작업을 수행할 때 필요한 여러 설정과 정보를 담고 있는 구조입니다.
coroutine context는 coroutine의 동작 방식을 결정하며, coroutine 이 어떤 스레드에서 실행될지, coroutine의 생명 주기를 어떻게 관리할지 등을 정의합니다.

Dispatchers
Dispatchers는 coroutine이 어떤 스레드에서 실행될지 결정하는 역할을 합니다.

  • Dispatchers.Default : 기본적으로 CPU 사용량이 많은 작업에 최적화되어 있습니다. 내부적으로 공유된 백그라운드 스레드 풀을 사용합니다. 주로 CPU를 많이 사용하는 작업, 예를 들어 큰 리스트 정렬이나 JSON 파싱 등에 사용됩니다.

  • Dispatchers.IO : 입출력, 네트워킹 작업에 최적화되어 있습니다. 파일, 데이터베이스, 네트워크 호출 등 I/O 작업을 실행하는 coroutine에 사용됩니다. 내부적으로 I/O 작업에 최적화된 스레드 풀을 사용하여 작업을 처리합니다.

  • Dispatchers.Main : 안드로이드에서 UI 작업을 수행할 때 사용됩니다. 메인 스레드에서 coroutine을 실행하며, UI를 업데이트하거나 메인 스레드에서만 호출 가능한 작업을 수행할 때 사용됩니다.

  • Dispatchers.Unconfined : 호출된 coroutine을 시작한 스레드에서 실행하지만, 첫 번째 suspension point 이후에는 호출된 suspend 함수에 의해 결정된 스레드에서 계속 실행됩니다. 특정 스레드에 구속되지 않으며, 주로 테스트나 특정한 케이스에서 사용됩니다. 하지만 공유 상태에 대한 접근이나 순서가 중요한 작업에는 사용을 피해야 합니다.


Job
Job은 coroutine의 생명 주기를 관리합니다.

<그림>

일반적인 coroutineActive 상태로 실행되지만 Job은 Coroutine의 생명주기를 관리할 수 있기 때문에 NEW 상태로 시작할 수 있으며 coroutine을 실행하고 싶을 때 실행하도록 할 수 있습니다.
coroutine이 실행이 일어나면 작업을 수행합니다.
마지막 명령이 실행되고 coroutine의 작업이 성공적으로 끝나게 되면 더 이상 coroutine을 재개할 수 없습니다.

coroutine 실행 중(Active 상태) ➡ coroutine 마지막 명령 수행(Completing 상태) ➡ coroutine 작업 완료(Complete 상태)

만약, coroutine이 수행 중에 취소 요청을 받으면 coroutine은 취소를 위한 정리 작업을 수행하도록 합니다.(Cancelling 상태)
coroutine이 취소되면 더 이상 실행할 수 없는 상태가 되어 coroutine은 완전히 종료되고 리소스 정리 작업이 완료된 상태가 됩니다.(Cancelled 상태)

일반적으로 Job 객체를 이용하여 이런 생명 주기를 관리할 때, coroutine이 수행이 끝나고(Complete 상태가 되었을 때) 필요한 작업을 해야 할 때 join() 메서드를 사용하고, 문제가 생겨서 coroutine을 취소해야 하는 경우 cancel() 메서드를 이용하여 coroutine 생명주기를 관리합니다.
특히 I/O 작업을 통해 결과에 따른 작업을 수행하기 위해 사용됩니다.
ex) api 호출이 이루어 졌을 때 A 작업 실패 시 B 작업

  • Job 사용 방법
val myJob = Job()
    val myScope = CoroutineScope(Dispatchers.IO + myJob)

    myScope.launch {
        delay(1000)
        println("job is executing")
    }.join()

    println("job completed")

// job is executing
// job completed

하지만 myJob의 Job() 객체를 생성하지 않아도 coroutine을 사용하면 Job() 객체를 자동으로 생성하여 context에 명시하지 않아도 Job이 자동으로 할당됩니다.

이외에도 다른 방법으로 사용 가능합니다.

val job = CoroutineScope(Dispatchers.IO).launch {
        delay(1000)
        println("job is executing")
    }

    job.join()

    println("job completed")

// job is executing
// job completed

첫 번째 예제는 Job을 명시적으로 생성하여 coroutineScope에 연결함으로써, 이 Job을 통해 scope 내의 모든 coroutine을 제어할 수 있습니다.
이러한 방식은 여러 coroutine이 같은 생명주기를 공유할 때 사용합니다.

두 번째 예제는 coroutine이 실행될 때 자동으로 생성되는 Job을 사용합니다. 이러한 방식은 개별 coroutine을 제어할 때 사용하며 간단하고 직관적입니다.



😊 마무리
앞선 시리즈 coroutine 내용과 이번 글을 통해 kotlin에서의 코루틴이 어떻게 동작되는지와 사용하는 방법을 알아보았습니다.
아무래도 coroutine이 장점이 많은 강력한 비동기 프로그래밍이기에 많은 사람들이 사용하고 있는 것 같습니다.
필자도 앞으로 프로그래밍 하면서 비동기 처리를 위해 coroutine을 많이 사용할 것 같습니다.


📕 참고자료

Coroutine Example Github
Coroutine Job

0개의 댓글