ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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.lifecyclemLifecycleRegistry 를 반환하고, Fragment.viewLifecycleOwnermViewLifecycleOwner 를 반환합니다.

     

     

    // 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 가 트리거되는 것을 알 수 있습니다.

     

    결론적으로

    mLifecycleRegistryonCreate() 이후에 ON_CREATE 가 트리거되며, onDestroy() 이전에 ON_DESTROY 가 트리거되는 생명주기를 가지는 객체이고

    mViewLifecycleOwneronActivityCreated() 이후에 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;
    }

     

    AutoCloseableclose() 라는 함수 하나만 가지고 있는 인터페이스입니다.

     

     

    // 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 가 생성됩니다.

     

    이때 CloseableCoroutineScopeAutoCloseable 속성과 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 에 저장하고 반환합니다.
    • CloseableCoroutineScopeAutoCloseable.close() 호출 시 인자로 제공된 coroutineContext 를 취소합니다.
    • ViewModel 에서 clear() 함수가 실행되면 내부적으로 관리하는 AutoClosable 객체들을 close() 실행을 통해 취소하며, 이때 저장되어 있던 viewModelScope 객체의 coroutineContext 이 CloseableCoroutineScope 구현 내용에 맞추어 취소됩니다.
    • 이를 통해 ViewModel 은 생명주기에 맞추어 coroutineContext 를 취소하고 있습니다.

    3. TL;DR;

    • lifecycleScopeLifecycleOwner 의 확장프로퍼티이며, 내부적으로 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 를 취소합니다.
Designed by Tistory.