안드로이드/Android 비동기 처리

[Android] CoroutineScope, CoroutineContext, CoroutineBuilder, suspend Function

코딩하는후운 2024. 3. 20. 13:29
반응형

#CoroutineScope

코루틴이 실행되는 범위로, 코루틴을 실행하고 싶은 Lifecycle에 따라 원하는 Scope를 생성하여 코루틴이 실행될 작업 범위를 지정할 수 있습니다.

  • 사용자 지정 CoroutineScope: CoroutineScope(CorountineContext)

ex) CoroutineScope(Dispatchers.Main) // Dispatchers.Main, Dispatchers.Default, Dispatchers.IO, Job()...

 

// 메인 쓰레드에서 실행될 사용자 정의 Scope
val scope = CoroutineScope(Dispatchers.Main)
scope.launch {
  // 메인 쓰레드 작업
}

// 백그라운드에서 실행될 사용자 정의 Scope
CoroutineScope(Dispatchers.IO).launch {
// 백그라운드 작업
}
  • GlobalScope : 앱이 실행될 때부터 종료될 때까지 실행
// 앱의 라이프사이클동안 실행될 Scope
GlobalScope.launch {
// 백그라운드로 전환하여 작업
    launch(Dispatchers.IO) {
    }

//메인 쓰레드로 전환하여 작업
    launch(Dispatchers.Main) {
    }
}

안드로이드 Jetpack라이브러리(AAC)에서는 코루틴을 쉽게 사용할 수 있도록 각 Lifecycler에 맞는 Scope를 제공해주고 있습니다.

 

  • ViewModelScope : ViewModel 대상, ViewModel이 제거되면 코루틴 작업이 자동으로 취소됩니다.
class MyViewModel: ViewModel() {
    init {
        viewModelScope.launch {
            //ViewModel이 제거되면 코루틴도 자동으로 취소됩니다.
        }
    }
}

 

  • LifecyclerScope : Lifecycle 객체 대상(Activity, Fragment, Service...), Lifecycler이 끝날 때 코루틴 작업이 자동으로 취소됩니다.
class MyActivity: AppcompatActivity() {
    init {
        lifecycleScope.launch {
            //Lifecycler이  끝날 때 코루틴 작업이 자동으로 취소됩니다.
        }
    }
}

 

  • LiveData : LiveData 대상, LiveData가 활성화되면 실행을 시작하고 비활성화되면 자동으로 취소됩니다.
val user: LiveData<User> = liveData {
    val data = repository.loadUser() //suspend function
    emit(data)
}

 

# CoroutineContext

코루틴 작업을 어떤 쓰레드에서 실행할 것인지에 대한 동작을 정의하고 제어하는 요소입니다.

주요 요소로는 Job과 Dispatchers가 있습니다.

  • Job : 코루틴을 고유하게 식별하고, 코루틴을 제어합니다.
val job = CoroutineScope(Dispatchers.IO).launch {
//비동기 작업
}

job.join() // 작업이 완료되기까지 대기
job.cancel() // 작업 취소
val job1 = Job()
CoroutineScope(job1 + Dispatchers.Main).launch {
//메인 쓰레드 작업

    launch(Dispatcher.IO) {
    //비동기 작업
    }

    withContext(Dispatchers.Default) {
    //비동기 작업
    }
}

val job2 = CoroutineScope(Dispatchers.IO).launch {
    //비동기 작업
}

job1.cancel()    // job1이 연결된 코루틴 작업 취소

Job instance를 CoroutineScope의 context로서 사용하고 싶은 경우 job instance의 이름을 포함하면 됩니다.

예를 들어 -> (Dispatchers.IO + job) 이렇게 명시하면 됩니다.

'+' 연산자는 다수의 코루틴 컨텍스트를 merge하는데 사용하면 됩니다.

 

cancel

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

아래 코드에서 job을 캔슬하게 되면 안에 있던 job1도 중단됩니다.

 

delay()는 yield()와 마찬가지로 다른 코루틴에 실행을 양보하게 된다. (정해준 시간이 끝날때까지 무한 양보한다. 즉, 양보했던 코루틴이 다시 양보했더라도 delay 시간이 끝나지 않았다면 다시 양보했던 코루틴에게 제어권이 넘겨진다.)

val job = CoroutineScope(Dispatchers.Default).launch {
        val job1 = launch {
            for(i in 0..10) {
                delay(500)
                Log.d("코루틴", "$i")
            }
        }
}

 

join

코루틴 내부에 여러 launch블록이 있는 경우 모두 새로운 코루틴으로 분기되어 동시 실행되기 때문에 순서를 정할 수 없습니다.

순서를 정해야 한다면 join()을 사용해서 순차적으로 실행되도록 코드를 짤수 있습니다.

CoroutineScope(Dispatchers.Default).launch {
    launch {
        for(i in 0..5) {
            delay(500)
        }
    }.join()

    launch {
        for(i in 6..10) {
            delay(500)
        }
    }
}

 

  • Dispatchers : 코루틴을 어떤 쓰레드에서 실행할 것인지에 대한 동작을 지정합니다.
Dispatchers.Main
안드로이드의 메인 쓰레드로, UI작업을 위해 사용해야 합니다.
예를 들어, UI를 구성하거나 suspending 함수, LiveData에서 수정사항을 가져오는 함수 등 작고, 가벼운 작업들만 실행할 때 사용됩니다.
권장사항은 코루틴을 메인스레드에서 시작하고 백그라운드 스레드로 변경하는 것입니다.
Dispatchers.IO
네트워크, 디스크 I/O 실행에 최적화되어 있습니다.
예를 들어, Retrofit으로 네트워크 통신을 하거나, File이나 Room 데이터베이스에서 데이터를 읽고/쓸 때 사용됩니다.
Dispatchers.Default
CPU 사용량이 많은 무거운 작업 처리에 최적화 되어 있습니다.
예를 들어, 데이터를 가공하거나 복잡한 연상, JSON파싱을 할 때 주로 사용됩니다.
Dispatchers.Unconfined
GlobalScope와 함께 사용됩니다.
코루틴이 현재 스레드에서 작동합니다. 중단되고 다시 시작되면 중단된 스레드(바뀌었다면 바뀐 스레드)에서 시작합니다.
안드로이드 앱 개발에서는 사용하지 않는 것을 권고합니다.

 

# CoroutineBuilder

위에서 설정한 CoroutineScope와 CoroutineContext를 통해 코루틴을 실행시켜주는 함수입니다.

주요 요소로는 launchasync가 있습니다.

  • launch : Job 객체이며, 결과값을 반환하지 않습니다.

실행 후 결과값이 필요 없는 모든 작업은 launch를 사용하여 실행할 수 있습니다.

CoroutineScope(Dispatchers.Main).launch {
//결과값이 필요없는 작업
}
  • async : Deffered 객체이며, 결과값을 반환합니다.

await() 함수를 사용하여, 코루틴 작업의 최종 결과값을 반환합니다.

val deffered = CoroutineScope(Dispatchers.Main).async {
// 결과값 "hello coroutine"
}

val message = deffered.await() // await() 함수로 결과값 반환
println(message)

Deffered 인터페이스는 job인터페이스를 확장한 것입니다. 그래서 우리는 Deffered인스턴스를 코루틴을 취소하는 것 같이 job처럼 사용할 수 있습니다.

 

  • withContext : async와 동일하게 결과값을 반환하며, async와의 차이점은 await()을 호출할 필요가 없다는 것입니다.

async{ }.await() 과 동일하다고 보면 됩니다.

 

코루틴 내부나 suspend 함수 안에서 구현이 가능하며, 콜백이 필요 없이 코드의 쓰레드 풀을 제어할 수 있기 때문에 네트워크 요청이나 DB조회 같은 작업에 주로 사용합니다.

init {
    viewModelScope.launch {    //Dispatchers.Main
        val user = getUserInfo()    //Dispatchers.Main
    }
}

suspend fun getUserInfo(): User =                            //Dispatchers.Main
        withContext(Dispatchers.IO) {                            //Dispatchers.IO
            val response = apiService.getUserInfo()        //Dispatchers.IO
            if (response.isSuccessful) {                            //Dispatchers.IO
                return@withContext response.body()        //Dispatchers.IO
            } else {                                                                //Dispatchers.IO
                return@withContext null                            //Dispatchers.IO
            }                                                                        //Dispatchers.IO
        }                                                                            //Dispatchers.Main

 

# suspend function

코루틴 안에서만 실행할 수 있는 코루틴 전용 메소드 입니다.

suspend 메소드는 일반적인 곳에서 호출할 수 없으며, 반드시 코루틴 안에서만 호출이 가능합니다.

 

이유는 코루틴의 실행이 일시중단(suspend) 되거나 다시 재개(resume)될 수 있기 때문에, 컴파일러에게 이 메소드는 코루틴안에서 실행할 메소드임을 정의하기 위해 메소드명 앞에 "suspend"를 붙여줘야 합니다.

suspend fun getUser(): User {
    ...
    return user
}

코루틴의 가장 큰 특징이라고 할 수 있는 suspend 키워드는 코루틴 안에서 사용되면 suspend 함수가 호출될 경우 이전까지의 코드의 실행이 멈추며 suspend 함수가 처리가 완료된 후 멈춰있던 원래 스코프의 다음 코드가 실행 됩니다.

suspend fun subRoutine() {
    for(i in 0..10) {
        Log.d("subroutine", "$i")
    }
}

CoroutineScope(Dispatchers.Main).launch {
    //선 처리 코드
    subRoutine()
    //후 처리 코드
}

suspend 함수를 코루틴 내에서 사용할 때 호출 이전까지 코드가 실행되고 호출이 된 순간은 해당 suspend함수가 처리가 모두 완료되어야 후 처리 코드가 실행되게 되는 것이죠.

이때 suspend 키워드를 사용했기 때문에 코루틴 스코프 안에서 자동으로 백그라운드 스레드처럼 동작하게 됩니다.

 

suspend 키워드를 붙인 함수가 실행되면서 호출한 쪽의 코드를 잠시 멈추게 되지만 스레드의 중단이 없기 때문입니다.

 

코루틴이 실행되다가 일시 정지하는 경우(일정 시간 대기 등) 코틀린 런타임은 해당 코루틴이 실행되던 스레으데 다른 코루틴을 할당하여 실행되게 한다.

그리고 다시 이전 코루틴이 재개할 때 사용 가능한 스레드를 코틀린 런타임이 할당해준다.

이런 것은 다 효율적인 스레드 활용을 위한 것인데, 이런 메커니즘에 맞게 실행되게 하는 함수가 바로 suspend 함수다.

 

코루틴을 사용하실 땐 세 가지 순서만 기억하세요!

1. 어떤 쓰레드에서 실행할 것인지 Dispatchers를 정하고
2. 코루틴이 실행될 Scope를 정하고
3. launch 또는 async로 코루틴을 실행 시키면 됩니다!

Context로 Scope를 만들고, Builder를 이용하여 코루틴을 실행!

 

 

참조 : 스터디그룹

반응형