-
lifecycleScope 와 viewModelScope 는 어떤 원리로 생명주기에 맞춰 코루틴을 취소하고 있을까?Android 2025. 4. 5. 00:00
안녕하세요. Mash-Up 15기 Android 팀으로 활동하고 있는 전계원입니다.
Android 에서는 Activity / Fragment / ViewModel 에서 Coroutine 을 생명주기에 맞게 사용하기 위해, lifecycleScope.launch { } 혹은 viewModelScope.launch { } 를 활용하고 있습니다.
그런데
lifecycleScope 와 viewModelScope 는 어떤 원리로 생명주기에 맞춰 코루틴을 취소하고 있을까요?
생명주기에 맞게 Coroutine 을 관리하는 원리가 궁금하였고, 본 포스팅을 통해 이러한 것들이 어떻게 가능했던 것인지 분석한 내용을 공유드리고자 합니다.
1. lifecycleScope 는 어떤 원리로 생명주기에 맞춰 코루틴을 취소하고 있을까?
1) LifecycleOwner.lifecycleScope 를 호출하면 어떤 일이 일어날까?
// LifecycleOwner.kt public interface LifecycleOwner { public val lifecycle: Lifecycle } /** * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle]. * * This scope will be cancelled when the [Lifecycle] is destroyed. * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]. */ public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() = lifecycle.coroutineScope
lifecycleScope 는 LifecycleOwner 의 확장프로퍼티입니다.
그리고 실 구현 내용은 lifecycle.coroutineScope 를 반환하고 있음을 알 수 있습니다.
// Lifecycle.kt public val Lifecycle.coroutineScope: LifecycleCoroutineScope get() { while (true) { val existing = internalScopeRef.get() as LifecycleCoroutineScopeImpl? if (existing != null) { return existing } val newScope = LifecycleCoroutineScopeImpl( this, SupervisorJob() + Dispatchers.Main.immediate ) if (internalScopeRef.compareAndSet(null, newScope)) { newScope.register() return newScope } } }
Lifecycle.coroutineScope 의 구현내용을 보면
- internalScopeRef 에 LifecycleCoroutineScopeImpl 이 저장되어 있으면 이를 반환합니다.
- 저장되어있지 않으면, SupervisorJob() + Dispatchers.Main.immediate 인 CoroutineContext 객체와 함께 새로운 LifecycleCoroutineScopeImpl 객체를 만들어 internalScopeRef 에 저장하고, newScope.register() 을 통해 생명주기를 관찰하도록 등록합니다.
위와 같은 방법에 따라 LifecycleCoroutineScopeImpl 객체를 생성함을 알 수 있습니다.
// Lifecycle.kt internal class LifecycleCoroutineScopeImpl( override val lifecycle: Lifecycle, override val coroutineContext: CoroutineContext ) : LifecycleCoroutineScope(), LifecycleEventObserver { init { if (lifecycle.currentState == Lifecycle.State.DESTROYED) { coroutineContext.cancel() } } fun register() { launch(Dispatchers.Main.immediate) { if (lifecycle.currentState >= Lifecycle.State.INITIALIZED) { lifecycle.addObserver(this@LifecycleCoroutineScopeImpl) } else { coroutineContext.cancel() } } } override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { if (lifecycle.currentState <= Lifecycle.State.DESTROYED) { lifecycle.removeObserver(this) coroutineContext.cancel() } } }
이때의 LifecycleCoroutineScopeImpl 의 구현체를 보면
- Lifecycle.State.INITIALIZED 일 때 addObserver 을 통해 등록한다
- onStateChanged 에서 Lifecycle 의 상태가 DESTROYED 로 변경되었을 때 removeObserver 를 통해 제거하고, cancel 을 통해 coroutineContext 를 취소한다
위와 같은 형태로 Lifecycle 에 맞추어 coroutineContext 를 취소하고 있음을 알 수 있습니다.
// LifecycleOwner.kt public interface LifecycleOwner { public val lifecycle: Lifecycle } /** * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle]. * * This scope will be cancelled when the [Lifecycle] is destroyed. * * This scope is bound to * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate]. */ public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope get() = lifecycle.coroutineScope
결론적으로 LifecycleOwner.lifecycleScope 는 Lifecycle State 가 DESTROYED 가 아닐 때 생명주기 관찰자로 등록하고, 생명주기 상태가 DESTROYED 로 변경되면 관찰자 등록해지, 코루틴취소 작업을 하는 LifecycleCoroutineScopeImpl 객체를 생성한다는 것을 알 수 있습니다.
2) Activity / Fragment 는 LifecycleOwner 이다.
우리가 사용하는 Activity 와 Fragment 는 모두 LifecycleOwner 를 구현하고 있다.
그렇기 때문에 Activity / Fragment 에서 lifecycleScope 를 통해 LifecycleOwner 에 정의된 생명주기에 맞게 코루틴을 생성하고, 취소할 수 있었습니다.
3) Fragment 에서 lifecycleScope 와 viewLifecycleOwner.lifecycleScope 은 다릅니다
Fragment 에서 생명주기에 맞는 코루틴 생성을 위해 lifecycleScope 를 사용할 수 있지만, viewLifecycleOwner.lifecycleScope 를 생성할 수도 있습니다. 그리고 이 둘은 비슷해 보이지만 다릅니다.
// Fragment.kt ... @Override @NonNull public Lifecycle getLifecycle() { return mLifecycleRegistry; } @MainThread @NonNull public LifecycleOwner getViewLifecycleOwner() { if (mViewLifecycleOwner == null) { throw new IllegalStateException("Can't access the Fragment View's LifecycleOwner when " + "getView() is null i.e., before onCreateView() or after onDestroyView()"); } return mViewLifecycleOwner; } ...
Fragment.lifecycle 은 mLifecycleRegistry 를 반환하고, Fragment.viewLifecycleOwner 는 mViewLifecycleOwner 를 반환합니다.
// Fragment.java ... public Fragment() { initLifecycle(); } private void initLifecycle() { mLifecycleRegistry = new LifecycleRegistry(this); ... } void performCreate(Bundle savedInstanceState) { ... if (Build.VERSION.SDK_INT >= 19) { mLifecycleRegistry.addObserver(new LifecycleEventObserver() { @Override public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) { if (event == Lifecycle.Event.ON_STOP) { if (mView != null) { Api19Impl.cancelPendingInputEvents(mView); } } } }); } ... onCreate(savedInstanceState); ... mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); } void performCreateView(...) { ... mViewLifecycleOwner = new FragmentViewLifecycleOwner(this, getViewModelStore()); mView = onCreateView(inflater, container, savedInstanceState); if (mView != null) { mViewLifecycleOwner.initialize(); ... } else { if (mViewLifecycleOwner.isInitialized()) { throw new IllegalStateException("Called getViewLifecycleOwner() but " + "onCreateView() returned null"); } mViewLifecycleOwner = null; } } void performActivityCreated(Bundle savedInstanceState) { ... onActivityCreated(savedInstanceState); ... restoreViewState(); ... } private void restoreViewState() { ... if (mView != null) { restoreViewState(mSavedFragmentState); } ... } final void restoreViewState(Bundle savedInstanceState) { ... onViewStateRestored(savedInstanceState); ... if (mView != null) { mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE); } } void performStart() { ... onStart(); ... mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START); if (mView != null) { mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_START); } ... } void performResume() { ... onResume(); ... mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); if (mView != null) { mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_RESUME); } ... } ...
Fragment 생명주기 위주로 코드를 살펴보면 다음과 같은 차이가 있음을 알 수 있습니다.
mLifecycleRegistry
- Fragment 객체 생성 시에 초기화
- onCreate() 이후에 ON_CREATE 트리거
- onStart() 이후에 ON_START 트리거
- onResume() 이후에 ON_RESUME 트리거
mViewLifecycleOwner
- onCreateView() 이후에 초기화(mViewLifecycleOwner#initialize)
- onActivityCreated() 이후에 ON_CREATE 트리거
- onStart() 이후에 ON_START 트리거
- onResume() 이후에 ON_RESUME 트리거
// Fragment.java void performPause() { ... if (mView != null) { mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); } mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE); ... onPause(); ... } void performStop() { ... if (mView != null) { mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_STOP); } mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_STOP); ... onStop(); ... } void performDestroyView() { ... if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState() .isAtLeast(Lifecycle.State.CREATED)) { mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); } ... onDestroyView(); ... } void performDestroy() { ... mLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); ... onDestroy(); ... }
mLifecycleRegistry
- onPause() 이전에 ON_PAUSE 트리거
- onStop() 이전에 ON_STOP 트리거
- onDestroy() 이전에 ON_DESTROY 트리거
mViewLifecycleOwner
- onPause() 이전에 ON_PAUSE 트리거
- onStop() 이전에 ON_STOP 트리거
- onDestroyView() 이전에 ON_DESTROY 트리거
그리고 위와 같은 차이로 ON_PAUSE, ON_STOP, ON_DESTROY 가 트리거되는 것을 알 수 있습니다.
결론적으로
mLifecycleRegistry 는 onCreate() 이후에 ON_CREATE 가 트리거되며, onDestroy() 이전에 ON_DESTROY 가 트리거되는 생명주기를 가지는 객체이고
mViewLifecycleOwner 는 onActivityCreated() 이후에 ON_CREATE 가 트리거되며, onDestroyView() 이전에 ON_DESTROY 가 트리거되는 생명주기를 갖는 객체임을 알 수 있습니다.
따라서 Fragment.lifecycleScope 를 기반으로 코루틴을 생성하면 Fragment 가 BackStack 에 추가되면, Fragment 가 사용자에게 보여지지 않음에도 코루틴이 취소되지 않으며 불필요한 메모리 공간을 차지하는 상황이 생길 수 있습니다.
그렇기에 Fragment 가 BackStack 에 추가되어 사용자에게 보이지 않을 때 코루틴을 자동으로 취소하기를 원한다면, Fragment.viewLifecycleOwner.lifecycleScope 을 사용하여 이와 같은 문제를 해결할 수 있습니다.
2. viewModelScope 는 어떤 원리로 생명주기에 맞춰 코루틴을 취소하고 있을까?
1) ViewModel.viewModelScope 를 호출하면 어떤 일이 일어날까?
// ViewModel.kt public expect abstract class ViewModel { ... } public val ViewModel.viewModelScope: CoroutineScope get() = synchronized(VIEW_MODEL_SCOPE_LOCK) { getCloseable(VIEW_MODEL_SCOPE_KEY) ?: createViewModelScope().also { scope -> addCloseable(VIEW_MODEL_SCOPE_KEY, scope) } } private val VIEW_MODEL_SCOPE_LOCK = SynchronizedObject()
viewModel 은 싱글톤보장 및 레이스컨디션 방지를 위해 viewModelScope 호출 시 VIEW_MODEL_SCOPE_LOCK 객체를 활용하고 있습니다.
viewModelScope 호출 시 getCloseable() 을 통해 가져오기를 시도하고, 존재하지 않으면 createViewModelScope() 를 통해 생성하고 addCloseable() 을 통해 저장합니다.
// ViewModel.jvm.kt public actual abstract class ViewModel { private val impl: ViewModelImpl? ... public actual fun addCloseable(key: String, closeable: AutoCloseable) { impl?.addCloseable(key, closeable) } public actual open fun addCloseable(closeable: AutoCloseable) { impl?.addCloseable(closeable) } ... public actual fun <T : AutoCloseable> getCloseable(key: String): T? = impl?.getCloseable(key) }
이때 addCloseable() 과 getCloseable() 은 ViewModelImpl 로 위임하고 있고
// ViewModelImpl.kt internal class ViewModelImpl { private val keyToCloseables = mutableMapOf<String, AutoCloseable>() ... fun addCloseable(key: String, closeable: AutoCloseable) { if (isCleared) { closeWithRuntimeException(closeable) return } val oldCloseable = synchronized(lock) { keyToCloseables.put(key, closeable) } closeWithRuntimeException(oldCloseable) } fun addCloseable(closeable: AutoCloseable) { if (isCleared) { closeWithRuntimeException(closeable) return } synchronized(lock) { closeables += closeable } } /** @see [ViewModel.getCloseable] */ fun <T : AutoCloseable> getCloseable(key: String): T? = @Suppress("UNCHECKED_CAST") synchronized(lock) { keyToCloseables[key] as T? } ... }
ViewModelImpl 내부에서는 keyToCloseables 라는 MutableMap(=LinkedHashMap) 에서 ViewModelScope 객체를 VIEW_MODEL_SCOPE_KEY 라는 key 값으로 저장/조회하는 것을 확인할 수 있습니다.
이때 저장되는 viewModelScope 객체는 AutoCloseable 이라는 자료형입니다.
2) AutoCloseable 과 CloseableCoroutineScope
// AutoCloseable.java public interface AutoCloseable { void close() throws Exception; }
AutoCloseable 은 close() 라는 함수 하나만 가지고 있는 인터페이스입니다.
// CloseableCoroutineScope.kt internal fun createViewModelScope(): CloseableCoroutineScope { val dispatcher = try { Dispatchers.Main.immediate } catch (_: NotImplementedError) { EmptyCoroutineContext } catch (_: IllegalStateException) { EmptyCoroutineContext } return CloseableCoroutineScope(coroutineContext = dispatcher + SupervisorJob()) } internal class CloseableCoroutineScope( override val coroutineContext: CoroutineContext, ) : AutoCloseable, CoroutineScope { constructor(coroutineScope: CoroutineScope) : this(coroutineScope.coroutineContext) override fun close() = coroutineContext.cancel() } ...
ViewModel 내부에 이미 생성된 viewModelScope 객체가 존재하지 않으면 createViewModelScope() 호출을 통해 viewModelScope 객체를 생성하는데, 이때 Dispatchers.Main.immediate + SupervisorJob() 인 coroutineContext 를 기반으로 CloseableCoroutineScope 가 생성됩니다.
이때 CloseableCoroutineScope 는 AutoCloseable 속성과 CoroutineScope 의 속성을 모두 갖는 객체이며, AutoCloseable.close() 가 실행되었을 때 CoroutineContext.cancel() 을 통해 코루틴을 취소하는 객체입니다.
3) ViewModel#clear() 가 실행되었을 때
// ViewModelImpl.kt internal class ViewModelImpl { ... @MainThread fun clear() { if (isCleared) return isCleared = true synchronized(lock) { for (closeable in keyToCloseables.values) { closeWithRuntimeException(closeable) } for (closeable in closeables) { closeWithRuntimeException(closeable) } // Clear only resources without keys to prevent accidental recreation of resources. // For example, `viewModelScope` would be recreated leading to unexpected behaviour. closeables.clear() } } ... private fun closeWithRuntimeException(closeable: AutoCloseable?) { try { closeable?.close() } catch (e: Exception) { throw RuntimeException(e) } } }
ViewModel 에서 clear() 함수가 실행되어 ViewModel 객체의 생명주기가 완료되었을 때 내부적으로 keyToCloseables 에 있는 모든 AutoCloseable 객체들을 close() 함수 실행을 통해 정리합니다.
그리고 여기에 저장된 viewModelScope 또한 close() 함수가 실행되며, 이는 내부적으로 CloseableCoroutineScope 의 구현에 따라 coroutineContext 를 취소할 것입니다.
결론적으로 정리하면 다음과 같습니다.
- viewModelScope 를 호출하면 내부 LinkedHashMap 에 저장된 값이 있는지 확인하고 없으면 createViewModelScope() 호출을 통해 CloseableCoroutineScope 를 생성하여 LinkedHashMap 에 저장하고 반환합니다.
- CloseableCoroutineScope 는 AutoCloseable.close() 호출 시 인자로 제공된 coroutineContext 를 취소합니다.
- ViewModel 에서 clear() 함수가 실행되면 내부적으로 관리하는 AutoClosable 객체들을 close() 실행을 통해 취소하며, 이때 저장되어 있던 viewModelScope 객체의 coroutineContext 이 CloseableCoroutineScope 구현 내용에 맞추어 취소됩니다.
- 이를 통해 ViewModel 은 생명주기에 맞추어 coroutineContext 를 취소하고 있습니다.
3. TL;DR;
- lifecycleScope 는 LifecycleOwner 의 확장프로퍼티이며, 내부적으로 Lifecycle.State.DESTROYED 가 아닐 때 생명주기 관찰자로 등록하고, Lifecycle.State.DESTROYED 로 상태가 변경되었을 때 관찰자 등록해지하고, coroutineContext 를 취소하는 LifecycleCoroutineScopeImpl 을 생성합니다.
- Fragment.lifecycleScope 는 Fragment 의 onCreate() ~ onDestroy() 기간 동안 살아있는 코루틴 스코프이며, Fragment.viewLifecycleOwner.lifecycleScope 는 Fragment 의 onActivityCreated() ~ onDestroyView() 기간동안 살아있는 코루틴 스코프입니다. 그래서 Fragment 가 BackStack 으로 이동할 때 coroutineContext 를 취소하고 싶다면 viewLifecycleOwner 를 활용해야 합니다.
- viewModelScope 는 ViewModel 의 확장프로퍼티이며, 내부 LinkedHashMap 에 생성했던 viewModelScope 객체를 저장/조회합니다. ViewModel 의 clear() 함수 호출 시 LinkedHashMap 에 저장된 모든 객체들의 close() 함수를 호출하며, 이때 viewModelScope 객체도 close() 함수가 호출되면서 CloseableCoroutineScope 구현 내용에 따라 coroutineContext 를 취소합니다.
'Android' 카테고리의 다른 글
Jetpack Compose에서 Custom Layout 구현해보기 (0) 2025.04.29 navigateUp과 popBackStack 비교 (뒤로가기 클릭 금지) (0) 2025.04.05 Compose PreviewParameterProvider 알아보기 (0) 2024.06.27 Jetpack Datastore 이해하기 (0) 2024.05.22 Fastlane으로 Android앱 자동 배포해보기 (feat. Firebase App Distribution) (1) 2024.04.20