본문 바로가기
도서/코틀린 코루틴

2장 코루틴 라이브러리 - 코루틴 스코프 만들기

by 안스 인민군 2024. 2. 10.

이번에는 스코프에 대해 배운것들을 요약해보고 일반적으로 사용하는 방법에 대해서 알아보자.

 

CoroutineScope 팩토리 함수

CoroutineScope는 CoroutineContext를 유일한 프로퍼티로 가지고 있는 인터페이스이다.

interface CoroutineScope {
	val coroutineContext: CoroutineContext
}

CoroutineScope 인터페이스를 구현한 클래스를 만들고 내부에서 코루틴 빌더를 직접 호출할 수 있다.

안드로이드에서 스코프 만들기

이번 예시는 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)
        }
    }
}

이제 스코프에서 컨텍스트를 정의해보자. 안드로이드에서는 메인 스레드가 많은 수의 함수를 호출해야 하므로 기본 디스패쳐 Dispatchers.Main으로 정의하는 것 이 좋다.

abstract class BaseViewModel : ViewModel() {
    protected val scope = CoroutineScope(Dispatchers.Main)
}

다음으로 스코프를 취소 가능하게 만들어야 한다. 인반적으로 스크린을 나가면 onDestroy 메서드 (Viewmodel에서는 onCleared)를 호출하면서 진행중인 모든 작업을 취소한다. 스코프를 취소하게 하려면 Job이 필요하다. 실제로는 CoroutineScope 함수가 잡을 추가하므로 따로 추가하지 않아도 되지만 명시적으로 작성하는 것 이 좋다.

abstract class BaseViewModel : ViewModel() {
    protected val scope =
        CoroutineScope(Dispatchers.Main + Job())

    override fun onCleared() {
        scope.cancel()
    }
}

전체 스코프 대신 스코프가 가지고 있는 자식 코루틴만 취소하는 것이 더 좋은 방법이다.

자식 코루틴만 취소하면 뷰 모델이 액티브한 상태로 유지하는 한, 같은 스코프에서 새로운 코루틴을 시작할 수 있다.

또한 Job을 사용하여 자식 코루틴 하나가 취소된 경우 부모와 다른 자식 코루틴 모두가 함께 취소되는 것을 막기 위해 SupervisorJob을 사용한다.

(이게 무엇을 의미하는지 모르겟음... baseViewModel을 상속받는 viewmodel들은 scope을 공유하기에 각자 끊어주는게 좋다는건가?? 그말은 아닌거 같은데....)

abstract class BaseViewModel : ViewModel() {
    protected val scope =
        CoroutineScope(Dispatchers.Main + SupervisorJob())

    override fun onCleared() {
        scope.coroutineContext.cancelChildren()
    }
}

 

마지막으로 잡히지 않는 예외처리를 처리하는 기본적인 방법이다.

안드로이드에서 다양한 종류의 예외가 발생했을 경우 취해야 할 행동을 정의한다. HTTP 호출로 401 Unauthorized를 받으면 로그인 창을 띄운다. 503 Service Unavailable 의 경우에는 서버에 문제가 생겼다는 메세지를 보여준다. 응답의 종류에 따라 대화창, 스낵바, 또는 토스트를 보여준다. BaseActivity에 예외 처리 핸들러를 한번만 정의해 두고 (생성자를 통해) 뷰 모델에 전달하는 방법이 많이 사용된다. 잡히지 않는 예외가 있는 경우 ExceptionHandler 를 사용해 해당 함수를 호출할 수 있다.

abstract class BaseViewModel(
    private val onError: (Throwable) -> Unit,
) : ViewModel() {
    private val exceptionHandler =
        CoroutineExceptionHandler { _, throwable ->
            onError(throwable)
        }
    private val context = Dispatchers.Main + SupervisorJob() + exceptionHandler
    protected val scope = CoroutineScope(context)

    override fun onCleared() {
        context.cancelChildren()
    }
}

BaseActivity나 다른 뷰 요소에서 라이브 데이터 프로퍼티로 예외를 가지고 있는 것도 예외를 처리하는 또 다른 방법이다.

abstract class BaseViewModel : ViewModel() {
    private val _failure: MutableLiveData<Throwable> = MutableLiveData()
    val failure: LiveData<Throwable> = _failure
    private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
        _failure.value = throwable
    }

    private val context = Dispatchers.Main + SupervisorJob() + exceptionHandler
    protected val scope = CoroutineScope(context)
    override fun onCleared() {
        context.cancelChildren()
    }
}

viewModelScope와 lifesycleScope

최근에는 안드로이드 애플리케이션에서 스코프를 따로 정의하는 대신에 viewModelScope를 사용할 수 있다.(androidx.lifecycle:lifecycle-runtime-ktx:2.2.0 이상 버젼이 필요)

Dispatchers.Main과 SupervisorJob을 사용하고, 뷰 모델이나 라이프사이클이 종료되었을때 잡을 취소시킨다는 점에서 우리가 만들었던 스코프와 거의 동일하다고 볼 수 있다.

// lifecycle-viewmodel-ktx 2.4.0 버젼에서 구현한 방식이다.
public val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTagIfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate),
        )
    }

internal class CloseableCoroutineScope(
    context: CoroutineContext,
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context
    override fun close() {
        coroutineContext.cancel()
    }
}

스코프에서 (CoroutineExceptionHandler와 같은) 특정 컨텍스트가 필요 없다면 viewModelScope와 lifecycleScope를 사용하는 것이 편리하고 더 좋다. 편리성 때문에 수많은 안드로이드 어플리케이션이 이 스코프를 사용하고 있다.

class ArticlesListViewModel(
    private val produceArticles: ProduceArticlesUseCase,
) : ViewModel() {
    private val _progressBarVisible = MutableStateFlow(false)
    val progressBarVisible: StateFlow<Boolean> = _progressBarVisible

    private val _articlesListState = MutableStateFlow<ArticlesListState>(Initial)
    val articlesListState: StateFlow<ArticlesListState> = _articlesListState
    fun onCreate() {
        viewModelScope.launch {
            _progressBarVisible.value = true
            val articles = produceArticles.produce()
            _articlesListState.value = ArticlesLoaded(articles)
            _progressBarVisible.value = false
        }
    }
}