이 타이머를 구현하면서 scope를 DI로 주입받았다.
DI로 scope를 주입받으면 결합도는 낮출 수 있다는 장점이 있고, 테스트 코드에서 테스트 스코프를 사용하기도 좋다.
하지만 여기서 한가지 문제점이 생각났다.
scope가 외부에서 주입되기 때문에, 사용하는 scope에 대한 정보를 알 수 없다.
그렇기에 scope가 SupervisorJob으로 생성되었는지 알 수 없다는 것이다.
Children of a supervisor job can fail independently of each other.
SupervisorJob이 사용되지 않은 경우, 같은 scope의 다른 코루틴이 fail되면 타이머의 코루틴도 같이 멈춰버릴 수 있다.
따라서 scope를 사용한 코루틴이 의도치 않은 타이밍에 cancel될 가능성이 있다는 것이다.
Creates a CoroutineScope with SupervisorJob and calls the specified suspend block with this scope. The provided scope inherits its coroutineContext from the outer scope, but overrides context's Job with SupervisorJob.
supervisorScope 블록은 현재 coroutineContext를 상속하면서 SupervisorJob을 가진 코루틴 스코프를 생성한다.
따라서 기존 scope의 생명주기에 따라 정상적으로 cancel은 되지만, 다른 코루틴으로부터는 영향을 받지 않을 수 있다.
또한, 이 스코프가 cancel되더라도 부모 스코프에는 영향을 주지 않게된다.
override fun start() {
if (timerJob?.isActive == true || _timerState.value is TimerState.Finished) return
timerJob?.cancel()
timerJob =
scope.launch(dispatchersProvider.default) {
supervisorScope { // scope with SupervisorJob
_timerState.value = TimerState.Running(_timerState.value.data)
while (_timerState.value is TimerState.Running && _timerState.value.remainingTime > 0) {
delay(100L)
_timerState.update {
val remainingTime = (it as TimerState.Running).data.remainingTime - 100L
if (remainingTime <= 0) {
TimerState.Finished(it.data.copy(remainingTime = 0))
} else {
TimerState.Running(it.data.copy(remainingTime = remainingTime))
}
}
}
}
}
}