ViewModel에서는 viewModelScope를 사용하라던데?
항상 코루틴을 사용하면서 MVVM 패턴을 적용할 때 ViewModel에서 viewModelScope를 사용하신 경험들이 다들 있으실 겁니다.
안드로이드 공식문서에서도 ViewModel에서 다음과 같이 사용하라고 정리되어있습니다.
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}
하지만 코루틴을 사용할 땐 Dispatcher를 지정해주는 것도 중요한데, 이런 부분을 간과하고 문서대로만 사용했다는 생각이 들어서 한번 내부적으로 정리해보려고 합니다.
viewModelScope의 내부 동작
viewModelScope은 다음과 같이 동작하고 있습니다.
private const val JOB_KEY = "androidx.lifecycle.ViewModelCoroutineScope.JOB_KEY"
/**
* [CoroutineScope] tied to this [ViewModel].
* This scope will be canceled when ViewModel will be cleared, i.e [ViewModel.onCleared] is called
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]
*/
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()
}
}
getTag를 통해서 scope가 null이 아니면 기존의 scope를 받고 아닐 경우 setTagIfAbsent를 통해 CloseableCoroutineScope를 생성하거나 특정 동작을 통해 처리하는 것 같습니다.
1) getTag는 이름은 tag를 가져오는 것인데 왜 CoroutineScope를 받는 것일까?
@SuppressWarnings({"TypeParameterUnusedInFormals", "unchecked"})
<T> T getTag(String key) {
if (mBagOfTags == null) {
return null;
}
synchronized (mBagOfTags) {
return (T) mBagOfTags.get(key);
}
}
getTag는 mBagOfTags가 null일 경우 null을 반환하고 mBagOfTags를 임계영역 처리하여 해당 key에 대한 값을 처리하고 있습니다.
이렇게 처리한 이유는 mBagOfTags가 Map으로 멀티스레드 환경에서 취약하기 때문입니다.
따라서 mBagOfTags에 값이 없을 경우 nul이며 이미 있는 경우엔 setTagIfAbsent 함수를 통해 CloseableCoroutineScope를 넣어줍니다.
2) setTagIfAbsent는 어떻게 동작할까?
@SuppressWarnings("unchecked")
<T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);
if (previous == null) {
mBagOfTags.put(key, newValue);
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {
// It is possible that we'll call close() multiple times on the same object, but
// Closeable interface requires close method to be idempotent:
// "if the stream is already closed then invoking this method has no effect." (c)
closeWithRuntimeException(result);
}
return result;
}
우선 setTagIfAbsent는 이미 key값에 해당하는 previous가 존재할 경우 previous를 반환하고, 없을 경우 newValue를 해당 key값에 넣어주고 반환하는 형태입니다. 이 때 Closeable 객체를 넣어줬던 이유는 clear() 함수가 실행되면 Closeable 객체들은 모두 소멸되기 때문입니다.
3) CloseableCoroutineScope는 뭘까?
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
CloseableCoroutineScope 함수는 Content로 SupervisorJob() + Dispatchers.Main.immediate를 CoroutineContext로서 받고 있습니다.
SupervisorJob
보통 코루틴의 경우 자식 코루틴에 에러가 생겼을 때 별도의 Exception Handler를 설정해주지 않으면 자식 코루틴이 부모 코루틴까지 취소시키게 됩니다. 부모 코루틴이 취소되면 당연히 나머지 자식 코루틴들도 모두 취소됩니다.
SupervisorJob의 경우 이러한 경우를 방지하여 해당 자식 코루틴이 에러가 발생해도 부모로 전파되지 않는 형태로 만들 수 있습니다.
Dispatchers.Main.immediate
Dispatchers.Main의 경우 Main Thread를 사용한다고 생각하시면 됩니다.
하지만 immediate가 붙은 이유는 다음과 같이 설명되고 있습니다.
Method may throw UnsupportedOperationException if immediate dispatching is not supported by current dispatcher, please refer to specific dispatcher documentation.
메소드는 만약 immediate dispatching이 현재 dispatcher에서 지원되지 않는다면 UnsupportedOperationException이 발생될 수 있습니다. dispatcher 문서를 참고해주세요.
Dispatchers.Main supports immediate execution for Android, JavaFx and Swing platforms.
Dispatchers.Main은 Android, JavaFx 그리고 Swing 플랫폼들에서 즉시 실행을 지원합니다.
immediate를 사용하게 되면 이미 해당 함수가 메인 스레드에 있다는 걸 의미하고 Main으로 디스패치를 요구하지 않습니다. 따라서 해당 함수가 Callback Queue에 등록되고 이후 Call Stack에서 비동기를 실행되는게 아닌, 즉시 동기로 실행됩니다.
예를 들어 보겠습니다.
fun main() {
CoroutineScope(Dispatchers.Main).launch {
println(1)
}
println(2)
}
위 코드를 실행한다면 println(1)이 디스패치가 필요하여 비동기로 작동되기 때문에 2가 먼저 출력되고 이후 1이 출력됩니다.
fun main() {
CoroutineScope(Dispatchers.Main.immediate).launch {
println(1)
}
println(2)
}
반면 위 코드를 실행한다면 println(1)이 immediate이기 때문에 디스패치가 필요하지 않아 동기로 작동하여 1, 2가 차례대로 출력됩니다.
끝!
제가 이번에 viewModelScope을 공부하면서 항상 당연하게 사용했던 것들이 사실 당연하지 않고 내부적으로 다 이유가 있었다는 것을 파악할 수 있었던 시간이었습니다. 앞으로도 조금씩 당연한 것들에 대해 파악하고 고민해보는 시간을 가져보려 합니다.
<참고>
'Android' 카테고리의 다른 글
[Android] implementation과 testImplementation의 차이 (1) | 2023.09.02 |
---|---|
[Android] Compose Navigation + Stateflow를 쓰는데 왜 리컴포지션이 계속 발생하지..? (1) (0) | 2023.08.07 |
[Android] strings.xml에서 %d%를 해결하는 방법 (1) | 2023.07.02 |
[Android] Service에 대한 정리 (0) | 2023.06.18 |
[Android] SharedPreference commit()과 apply() 중 어떤 걸 써야할까? (0) | 2023.05.23 |
댓글