티스토리 뷰

728x90

 

 

이전 포스팅에서 코루틴 스코프를 적절하게 만드는 방법에 대해 배웠다.

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

 

CoroutineScope 팩토리 함수

interface CoroutineScope {
    val coroutineContext: CoroutineContext
}

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

 

class SomeClass : CoroutineScope {
    override val coroutineContext: CoroutineContext = Job()
    fun onStart() {
        launch {
            // ...
        }
    }
}

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

하지만 이런 방법은 자주 사용하지 않는다.

CoroutpineScope 인터페이스를 구현한 클래스에서 CoroutineScope의 함수(cancel(), ensureActive() 등)를 직접 호출하면 예기치 못한 문제가 발생한다.

갑자기 내부에서 전체 스코프를 취소하면 코루틴이 시작될 수 없다.

 

class SomeClass {
    val scope: CoroutineScope = ...

    fun onStart() {
        scope.launch {
            // ...
        }
    }
}

스코프를 프로퍼티로 가지고 있다가 코루틴 빌더를 호출할 사용하는 방식이 일반적이다.

 

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 =
        "CoroutineScope(coroutineContext=$coroutineContext)"
}

CoroutineScope 팩토리 함수를 사용하면 스코프 객체를 쉽게 만들 수 있다.

함수는 컨텍스트를 넘겨 받아 스코프를 만든다.

(인자로 전달받은 컨텍스트에 잡이 없으면 구조화된 동시성을 위해 을 추가하기도 한다)

 

 

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

MVVM나 MVP 아키텍처를 주로 사용하는 안드로이드에서는 사용자에게 보여줄 데이터가 ViewModel이나 Presenter에 존재한다.

일반적으로 ViewModel이나 Presenter에서 코루틴이 가장 먼저 시작된다.

UseCase나 Repository와 같은 다른 계층에서는 보통 중단 함수를 사용한다.

루틴을 Fragment나 Activity에서 코루틴이 시작될 수도 있다.

 

어느 곳에서 코루틴이 시작되든지 코루틴을 만드는 방법은 비슷하다.

MainViewModel의 onCreate() 함수에서 데이터를 가져오는 경우를 예로 들어 보자.

 

abstract class BaseViewModel : VievModel() {
    protected val scope = CoroutineScope(TODO())
}

class MainViewModel(
    private val userRepo: UserRepository,
    private val newsRepo: NewsRepository,
) : BaseVievModel {
    fun onCreate() {
        scope.launch {
            val user = userRepo.getUser()
            view.showUserData(user)
        }
        scope.launch {
            val news = newsRepo.getNews()
                .sortedByDescending { it.date }
            view.showNews(news)
        }
    }
}

 

 

BaseViewModel에서 스코프를 만들면, 모든 ViewModel에서 사용될 스코프를 한 번만 정의할 수 있다.

 

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

 

이제 BaseViewModel의 스코프를 정의해야 한다.

안드로이드에서는 메인 스레드가 UI 작업을 담당하므로, UI 작업을 실행할 때의 기본 디스패처를 Dispatchers.Main으로 설정한다.

 

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

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

다음으로 스코프를 취소할 수 있도록 잡을 추가한다.

CoroutineScope 함수가 잡을 추가하므로 따로 추가하지 않아도 되지만, 이 방식이 좀 더 명시적이다.

 

이제 사용자가 화면을 벗어나서 ViewModel이 소멸되면  onCleared() 함수가 호출되고, 스코프가 취소되어 진행 중인 모든 작업을 취소한다.

 

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

    override fun onCleared() {
        scope.coroutinContext.cancelchildren()
    }
}

전체 스코프를 취소하는 것보다 스코프의 자식 코루틴만 취소하는 것이 더 좋다.

자식 코루틴만 취소하면 ViewModel이 Active 상태인 경우 스코프의 새로운 코루틴을 시작할 수 있다.

 

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

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

 

하나의 스코프에서 시작된 각각의 코루틴이 독립적으로 작동해야 할 때가 있다.

예를 들어 사용자 데이터를 가져올 때 예외가 발생하더라도, 기존에 있는 뉴스 데이터는 계속 볼 수 있어야 한다.

을 사용한 경우 자식 코루틴 하나가 취소된다면 부모와 다른 자식 코루틴 모두 취소된다.

자식 코루틴이 독립적으로 작동하려면 Job 대신 SupervisorJob을 사용한다.

 

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()
    }
}

 

보통 안드로이드에서는 예외의 종류에 따라 취해야 할 행동이 다르다.

예를 들어 HTTP 응답으로 401을 받으면 로그인 창을 띄우고, 503을 받으면 서버에 문제가 생겼다는 메시지를 보여준다.

BaseActivity에서 예외에 대한 처리를 한 번만 정의해두고, ViewModel의 생성자에 전달하는 방식이 일반적이다.

ViewModel에서는 CoroutineExceptionHandler을 통해 잡히지 않은 예외를 처리한다.

 

abstract class BaseViewModel : ViewModel() {
    private val _failure: MutableLiveData<Throwable> =
        MutableLiveData()
    val failure: LiveData<Throwable> = _failure

    private val exceptionHandler =
        CoroutineExceptionHandler { _, trowable ->
            _failure.value = throwable
        }

    private val context =
        Dispatchers.Main + SupervisorJob() + exceptionHandler

    protected val scope = CoroutineScope(context)

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

BaseActivity나 BaseViewModel에서 LiveData 타입 프로퍼티로 예외를 가지고 있는 것도 예외를 처리하는 방법 중 하나다.

 

 

viewModelScope와 lifecycleScope

val ViewModel.viewModelScope: CoroutineScope
    get() {
        val scope: CoroutineScope? = this.getTag(JOB_KEY)
        if (scope != null) {
            return scope
        }
        return setTaglfAbsent(
            JOB_KEY,
            CloseableCoroutineScope(
                SupervisorJob() + Dispatchers.Main.immediate
            )
        )
    }

internal class CloseableCoroutineScope(
    context: CoroutineContext
) : Closeable, CoroutineScope {
    override val coroutineContext: CoroutineContext = context

    override fun close() {
        coroutineContext.cancel()
    }
}

안드로이드에서는 스코프를 따로 정의하는 대신에 viewModelScope 또는 lifecycleScope를 사용할 수도 있다.

viewModelScope과 lifecycleScope는 위에서 만들었던 스코프와 거의 동일하다.

Dispatchers.Main과 SupervisorJob 을 사용하며, ViewModel(viewModelScope)이나 Lifecycle(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
        }
    }
}

스코프에서 특정 컨텍스트(예를 들어 CoroutineExceptionHandler)의 추가가 필요하지 않다면 viewModelScopelifecycleScope를 사용하는 것이 훨씬 편리하다.

 

 

추가적인 호출을 위한 스코프 만들기

val analyticsScope = CoroutineScope(SupervisorJob())

 

이전에 설명한 것처럼, 로그 수집이나 통계와 같은 추가적인 연산을 위한 스코프를 따로 만드는 경우가 있다.

이런 스코프는 함수나 생성자의 인자를 통해 주입된다.

호출을 중단하기 위한 목적으로만 스코프를 사용하는 경우 SupervisorScope사용하는 것만으로 충분하다.

 

private val exceptionHandler =
    CoroutineExceptionHandler { _, throwable ->
        FirebaseCrashlytics.getlnstance()
            .recordException(throwable)
    }

val analyticsScope = CoroutineScope(SupervisorJob() + exceptionHandler)

모든 예외를 모니터링 서비스에 보내고 싶다면 CoroutineExceptionHandler를 사용한다.

 

val analyticsScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

추가적인 연산을 위한 스코프에 다른 디스패처를 설정하는 것도 자주 사용된다.

스코프에서 블로킹 호출을 한다면 Dispatchers.IO를, 안드로이드의 UI를 다뤄야 한다면 Dispatchers.Main을 사용한다.

Dispatchers.Main으로 설정하면 안드로이드에서 테스트하기가 쉬워진다.

 

 

 

출처

https://www.yes24.com/product/goods/123034354

728x90
250x250
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/01   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함