안드로이드에서 스코프 만들기
대부분의 안드로이드 애플리케이션에는 MVC 모델을 기반으로 한 MVVM이나 MVP 아키텍처가 사용되고 있다. 이러한 아키텍처에서는 사용자에게 보여 주는 부분을 `ViewModels` 나 `Presenters`와 같은 객체로 추출한다. (일반적으로 코루틴이 가장 먼저 시작되는 객체)
`UseCase` 나 `Repository`와 같은 다른 계층에서는 보통 중단 함수를 사용한다.
다음 코드는 `BaseViewModel`을 아래의 조건을 만족하여 만든 것이다.
- 모든 뷰모델에서 쓰일 스코프를 단 한 번으로 정의
- 메인 디스패처로 컨텍스트 정의
- 스코프를 취소 가능하게 만들되, 스코프가 가지고 있는 자식 코루틴만 취소
- 스코프에서 시작된 각각의 코루틴이 독립적으로 작동
- 잡히지 않는 예외 처리
abstract class BaseViewModel : ViewModel() {
private val _failure: MutableLiveData<Throwable> =
MutableLiveData()
val failure: LiveData<Throwable>
get() = _failure
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
_failure.value = throwable
}
// Dispatcher.Main으로 컨텍스트 정의 + 스코프에서 시작된 각각의 코루틴이 독립적으로 작동 + 잡히지 않는 예외 처리
private val context = Dispatchers.Main + SupervisorJob() + exceptionHandler
// 모든 뷰모델에서 쓰일 스코프를 단 한 번으로 정의
protected val scope = CoroutineScope(context)
override fun onCleared() {
context.cancelChildren() // 스코프를 취소 가능하게 만들되, 스코프가 가지고 있는 자식 코루틴만 취소
}
}
viewModelScope와 lifecycleScope
위에 처럼 스코프를 따로 정의할 수 있지만 `viewModelScope` 또는 `lifecycleScope` 를 사용할 수 있다. (대부분 이를 사용할 것이다.)
구현부를 보면,
1) `Dispatchers.Main`과 `SupervisorJob`을 사용한다는 점
2) 뷰모델이나 라이프 사이클이 종료되었을 때 잡을 취소시킨다는 점
에서 위에서 만들었던 `BaseViewModel`과 거의 동일하다고 볼 수 있다.
스코프에서 `CoroutineExceptionHandler`와 같은 특정 컨텍스트가 필요 없다면 `viewModelScope` 와 `lifecycleScope`를 사용하는 것이 편리하다.
백엔드에서 코루틴 만들기
스프링부트는 컨트롤러 함수가 suspend로 선언되는 것을 허용한다.
그러나 따로 스코프를 만들어야 한다면 다음과 같은 것들이 필요하다.
- 스레드 풀(또는`Dispatchers.Default`)을 가진 커스텀 디스패처
- 각각의 코루틴을 독립적으로 만들어 주는 `SupervisorJob`
- 적절한 에러 로그를 남기고 처리하는 CoroutineExceptionHandler
생성자를 통해 커스텀하게 만들어진 스코프를 클래스로 주입되는 방법이 가장 많이 사용된다.
스코프는 한 번만 정의되면 수많은 클래스에서 활용될 수 있으며, 테스트를 위해 다른 스코프로 쉽게 대체할 수도 있다.
@Configuration
class CoroutineScopeConfig {
@Bean("coroutineDispatcher")
fun coroutineDispatcehr(): CoroutineDispatcher = Dispatchers.IO.limitedParallelism(5)
@Bean("coroutineExceptionHandler")
fun coroutineExceptionHandler(): CoroutineExceptionHandler
= CoroutineExceptionHandler { _, e ->
logger.error(e)
}
@Bean
fun coroutineScope(
coroutineDispatcher: CoroutineDispatcher,
coroutineExceptionHandler: CoroutineExceptionHandler,
) = CoroutineScope(
SuperVisorJob() + coroutineDispatcher + coroutineExceptionHandler
)
}
추가적인 호출을 위한 스코프 만들기
추가적인 연산이 필요할 경우 메서드 내에서 생성하여 함수나 생성자의 인자를 통해 주입하는 방식을 사용한다.
스코프를 호출을 중단하기 위한 목적으로만 사용하려는 경우 `SupervisorScope`를 사용하는 것만으로 충분하다.
val analyticsScope = CoroutinesCope(SupervisorJob())
마무리
현업에서 코루틴을 사용할 때 스코프를 만드는 건 중요하다.
지금까지 배운 것으로 작고 간단한 애플리케이션을 만드는 데는 충분하지만, 좀 더 거대한 프로젝트를 진행한다면 적절한 동기화와 테스트라는 주제에 대해 알아야 한다.
참고자료
마르친 모스카와(2023), 코틀린 코루틴 Kotlin Coroutines : Deep Dive - 안드로이드 및 백엔드 개발자를 위한 비동기 프로그래밍, 인사이트
'Kotlin Coroutines' 카테고리의 다른 글
[Kotlin Coroutines] 공유 상태로 인한 문제 (0) | 2024.06.03 |
---|---|
[Kotlin Coroutines] 디스패처 (1) | 2024.05.27 |
[Kotlin Coroutines] 코루틴 스코프 함수 (0) | 2024.05.20 |
[Kotlin Coroutines] 코루틴 예외 처리 (0) | 2024.05.17 |
[Kotlin Coroutines] 코루틴 취소 (0) | 2024.05.17 |