이전 장들에서 스코프를 적절하게 만드는 방법에 대해 배웠다. 이번에는 스코프에 대해 배운 것들을 요약해 보고 일반적으로 사용하는 방법에 대해 알아보자.
CoroutineScope는 coroutineContext를 유일한 프로퍼티로 가지고 있는 인터페이스다.
public interface CoroutineScope {
/**
* The context of this scope.
* Context is encapsulated by the scope and used for implementation of coroutine builders that are extensions on the scope.
* Accessing this property in general code is not recommended for any purposes except accessing the [Job] instance for advanced usages.
*
* By convention, should contain an instance of a [job][Job] to enforce structured concurrency.
*/
public val coroutineContext: CoroutineContext
}
CoroutineScope 인터페이스를 구현한 클래스를 만들고 내부에서 코루틴 빌더를 직접 호출할 수 있다.
class SomeClass : CoroutineScope {
override val coroutineContext: CoroutineContext = Job()
fun onStart() {
launch {
// ...
}
}
}
하지만 이런 방법은 자주 사용되지 않는다. 얼핏 보면 편리한 것 같지만, CoroutineScope를 구현한 클래스에서 cancel이나 ensureActive 같은 다른 CoroutineScope의 메서드를 직접 호출하면 문제가 발생할 수 있다. 갑자기 전체 스코프를 취소하면 코루틴이 더 이상 시작될 수 없다. 대신 코루틴 스코프 인스턴스를 프로퍼티로 가지고 있다가 코루틴 빌더를 호출할 때 사용하는 방법이 선호된다.
class SomeClass {
val scope: CoroutineScope = TODO()
fun onStart() {
scope.launch {
// ...
}
}
}
코루틴 스코프 객체를 만드는 가장 쉬운 방법은 CoroutineScope 팩토리 함수를 사용하는 것이다. 이 함수는 컨텍스트를 넘겨 받아 스코프를 만든다(Job이 Context에 없으면 구조화된 동시성을 위해 Job을 추가할 수도 있다).
<aside> 📢 생성자처럼 보이는 함수는 가짜 생성자로 알려져 있다. 이펙티브 코틀린 - ‘Item33 : 생성자 대신 팩토리 함수를 사용하라’ 에 설명되어 있다.
</aside>
public fun CoroutineScope(
context: CoroutineContext
): CoroutineScope =
ContextScope(
if (context[Job] != null) context
else context + Job()
)
internal class ContextScope(
context: CoroutineContext
) : CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun toString(): String {
return "CoroutineScope(coroutineContext=$coroutineContext)"
}
}
대부분의 안드로이드 애플리케이션에서는 MVC 모델을 기반으로 한 MVVM이나 MVP 아키텍처가 사용되고 있다. 이러한 아키텍처에서는 사용자에게 보여 주는 부분을 ViewModels나 Presenters와 같은 객체로 추출한다. 일반적으로 코루틴이 가장 먼저 시작되는 객체다.
Use Case나 Repository와 같은 다른 계층에서는 보통 suspend 함수를 사용한다. 코루틴을 Fragment나 Activity에서 시작할 수도 있다. 안드로이드의 어떤 부분에서 코루틴을 시작하든지 간에 코루틴을 만드는 방법은 모두 비슷하다. (사용자가 스크린을 열 때 호출하는) onCreate를 통해 MainViewModel이 데이터를 가져오는 경우를 예로 들어보자. 특정 스코프에서 시작한 코루틴이 데이터를 가지고 오는 작업을 수행해야 한다. BaseViewModel에서 스코프를 만들면, 만든 ViewModel에서 쓰일 스코프를 단 한 번으로 정의한다. 따라서 MainViewModel에서는 BaseViewModel의 scope 프로퍼티를 사용하기만 하면 된다.
abstract class BaseViewModel : ViewModel() {
protected val scope = CoroutineScope(TODO())
}
class MainViewModel(
private val userRepo: UserRepository,
private val newsRepo: NewsRepository,
) : BaseViewModel() {
fun onCreate() {
scope.launch {
val user = userRepo.getuser()
view.showUserData(user)
}
scope.launch {
val news = newsRepo.getNews()
.sortedByDescending { it.date }
view.showNews(news)
}
}
}
이제 Scope에서 Context를 정의해 보자. 안드로이드에서는 메인 스레드가 많은 수의 함수를 호출해야 하므로 기본 디스패처를 Dispatchers.Main으로 정하는 것이 가장 좋다. 안드로이드의 기본 컨텍스트로 메인 디스패처를 사용하겠다.
abstract class BaseViewModel : ViewModel() {
protected val scope = CoroutineScope(Dispatchers.Main)
}