이 시리즈에서는 안드로이드의 Lifecycle Architecture Component의 원리와 활용법을 알아봅니다.
모든 생명체는 생성부터 소멸까지의 과정을 겪습니다. 또한, 여러 생물들끼리는 자신만의 특성을 가지고 상호작용하면서 자신들이 존재하는 생태계에 변화를 일으키기도 합니다.
안드로이드 생태계에서도 자연환경과 크게 다르지 않은 모습을 볼 수 있습니다. 안드로이드 측에서 이러한 현상을 효율적으로 관리하기 위해 여러 수단들을 제공하는데, 이어질 내용에서 자세히 알아보도록 하겠습니다.
생물들에게 수명이 주어지듯, 안드로이드의 컴포넌트 역시 수명 주기에 따라 동작하게 됩니다.
자연에서 생물은 시간의 흐름이라는 이벤트에 따라 탄생, 성장, 성숙, 노화, 소멸의 과정을 거치게 됩니다(관점에 따라 이벤트 및 과정은 다를 수 있습니다). 안드로이드 컴포넌트의 생명주기 또한 상태(States)를 가지며, 특정 이벤트(Events)에 의해 다른 상태로 전환됩니다. 바꿔 말하면 Activity나 Fragment, View 같은 컴포넌트들은 안드로이드의 Lifecycle에 따라 생성되고 활성화되며 최종적으로 소멸하는 과정을 거칩니다.
아래 그림은 안드로이드 공식 문서에서 제공하는 컴포넌트 생명 주기 다이어그램으로, 각 상태와 이벤트 간의 전환 과정을 시각적으로 보여줍니다.
안드로이드의 Lifecycle은 5가지 상태를 가집니다.
안드로이드의 Lifecycle에서, 6가지 이벤트들이 상태 간의 전이를 유발합니다.
추가적으로 ON_ANY
라는 이벤트도 존재하는데, Lifecycle의 Event로 정의는 되어 있지만, 실제로는 프레임워크 내부에서만 사용되어야 하며 개발자가 직접 사용하면 안 되는 예약 이벤트입니다. 만약 개발자가 해당 이벤트를 발생시키는 것을 시도한다면 IllgalArgumentException
예외가 발생합니다(이벤트가 디스패치 되는 방식은 추후 설명합니다).
이러한 유한 상태 기계(FSM, Finite State Machine) 기반 구조에서 우리는 실제 자연 현상과의 유사성을 발견할 수 있습니다. 컴포넌트는 CREATED, STARTED 상태를 거쳐 가장 활발한 RESUMED 시기에 도달합니다. 이후 점차 쇠락하며 다시 STARTED, CREATED라는 기초 상태로 돌아갔다가, 마침내 DESTROYED에 이르게 되는데, 이는 생명체가 성장과 절정을 지나 쇠퇴와 소멸에 이르는 과정과 닮아 있습니다.
Lifecycle에는 해당 생명주기 안에서만 동작하도록 하는 코루틴을 생성할 수 있도록 하는 확장 프로퍼티 coroutineScope
가 정의되어 있습니다.
public val Lifecycle.coroutineScope: LifecycleCoroutineScope
LifecycleCoroutineScopeImpl
을 살펴보면, 모든 세부 구현을 완전히 이해하기는 어렵지만, 여러 조건에 따라 cancel()
이 호출되는 것을 확인할 수 있습니다. (참고로 Kotlin의 enum 클래스는 선언된 순서대로 compareTo()
연산을 지원하며, State
의 경우 DESTROYED
, INITIALIZED
, CREATED
, STARTED
, RESUMED
순으로 정의되어 있습니다.)
결국 이 스코프는 라이프사이클이 INITIALIZED
상태가 되면 자동으로 해당 라이프사이클 내부에서 코루틴을 실행할 수 있도록 하고, 라이프사이클이 DESTROYED
상태가 되면 이미 생성된 코루틴을 자동으로 취소(cancel
)하여 메모리 누수를 방지하는 역할을 수행합니다.
internal class LifecycleCoroutineScopeImpl(
override val lifecycle: Lifecycle,
override val coroutineContext: CoroutineContext
) : LifecycleCoroutineScope(), LifecycleEventObserver {
init {
// LifecycleScope를 생성할 때 이미 LifecycleOwner가 DESTROYED 상태라면 즉시 cancel() 호출
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 {
// 아직 INITIALIZED되지 않은 상태이면 코루틴 취소
coroutineContext.cancel()
}
}
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (lifecycle.currentState <= Lifecycle.State.DESTROYED) {
lifecycle.removeObserver(this)
// DESTROYED 상태에 도달했으므로 코루틴 취소
coroutineContext.cancel()
}
}
}
자연 현상에 비유하면, 벌은 꽃이 피는 시기를 감지하고 꿀을 채취하며, 꽃이 시들면 다른 꽃으로 이동합니다. 이처럼 앱에서도 특정 화면이나 컴포넌트가 활성화되었을 때 필요한 동작을 수행하고, 화면이 비활성화되면 자원을 절약하기 위해 동작을 중단해야 할 것입니다.
이러한 동작을 지원하기 위해 Lifecycle
추상 클래스는 Observer 패턴을 기반으로, 상태 변화를 감지할 수 있는 관찰자를 추가하고 제거하는 기능을 제공합니다.
@MainThread public abstract fun addObserver(observer: LifecycleObserver)
@MainThread public abstract fun removeObserver(observer: LifecycleObserver)
addObserver(observer: LifecycleObserver)
STARTED
상태라면, observer는 먼저 ON_CREATE
, 이어서 ON_START
이벤트를 받게 됩니다.removeObserver(observer: LifecycleObserver)
결국 Lifecycle은 상태(State)와 이벤트(Event)를 기반으로 컴포넌트를 관찰 가능하게 만들어, 화면이나 앱 상태에 맞춰 자동으로 동작을 시작하고 중단할 수 있도록 하는 구조로 구현되어 있음을 알 수 있습니다.
생명주기를 살펴보았으니, 이제 그 생명주기를 가지는 소유물을 살펴볼 수 있습니다.
안드로이드에는 LifecycleOwner
라는 이름의 인터페이스가 존재하는데, 이름 그대로 Lifecycle
하나의 프로퍼티를 가지고 있는 모습입니다.
public interface LifecycleOwner {
public val lifecycle: Lifecycle
}
이 간단해 보이는 인터페이스가 사실은 전체 생명주기 시스템의 핵심입니다. 모든 생물이 '생명'이라는 공통된 특성을 가지는 것처럼, 모든 LifecycleOwner는 자신만의 Lifecycle 인스턴스를 소유합니다.
대표적으로 우리가 사용하는 Activity
와 Fragment
는 모두 이 인터페이스를 구현하고 있습니다. 이 두 가지 컴포넌트는 자신의 Lifecycle을 가지고 있으며, 생명주기 변화에 따라 우리에게 너무나도 익숙한 콜백(onCreate
, onStart
, onResume
등)의 동작을 정의하는 것이 일반적입니다.
즉, LifecycleOwner
는 자신의 상태(State)를 관리하고, 상태 변화에 따라 Observer에게 알림을 전달하는 역할을 하며, 이를 통해 앱 내 다양한 컴포넌트가 생명주기에 안전하게 반응할 수 있도록 합니다.
이 인터페이스를 활용하면 Activity, Fragment뿐만 아니라 커스텀 컴포넌트도 자신의 생명주기를 정의하고, Lifecycle에 종속적인 동작을 구현할 수 있는데, 관련 내용은 이후 살펴보도록 하겠습니다.
레지스트리(Registry)란 정보를 등록하고 관리하는 저장소를 의미합니다.
LifecycleRegistry는 Lifecycle을 구현한 클래스로, 대표적으로 다음과 같은 정보를 관리합니다.
public actual open class LifecycleRegistry private constructor(
provider: LifecycleOwner,
private val enforceMainThread: Boolean
) : Lifecycle() {
// ...
// 관찰자들
private var observerMap = FastSafeIterableMap<LifecycleObserver, ObserverWithState>()
// 현재 생명주기
private var state: State = State.INITIALIZED
actual override var currentState: State
get() = state
/**
* Moves the Lifecycle to the given state and dispatches necessary events to the observers.
*
* @param state new state
*/
set(state) {
enforceMainThreadIfNeeded("setCurrentState")
moveToState(state)
}
// ...
}
Dispatch라는 용어는 글자 뜻 그대로 컴포넌트에서 발생한 이벤트를 다른 컴포넌트로 전달한다는 뜻입니다. 자연에서 한 생물의 상태 변화가 생태계 전체에 파급되는 것과 같은 메커니즘입니다.
하지만 왜 ‘다른 컴포넌트’로 이벤트를 전달해야 하는 것인지 의문이 들 수 있습니다. 자연에서 벌이 꽃의 개화 시기를 알아야 꿀을 채취할 수 있고, 철새가 계절 변화를 감지해야 적절한 시기에 이동할 수 있듯이, 안드로이드에서도 다양한 컴포넌트들이 서로의 생명주기 상태를 알아야 적절히 동작할 수 있습니다. 이것이 바로 이벤트 전달이 필요한 이유입니다.
그렇다면 여기서 가리키는 '다른 컴포넌트'는 해당 컴포넌트의 생명주기를 관찰하는 컴포넌트(LifecycleObserver)들임을 쉽게 유추해볼 수 있습니다. 이와 관련한 자세한 정보는 이후 내용에서 다루도록 하겠습니다.
앞서 살펴본 LifecycleRegistry
구현의 currentState
라는 커스텀 프로퍼티를 다시 살펴보겠습니다.
actual override var currentState: State
get() = state
set(state) {
// ...
moveToState(state)
}
currentState에 새로운 값을 할당하면 즉각적으로 상태가 바뀌는 것이 아니라 moveToState
라는 메서드가 호출되는데, 이 함수의 동작이 바로 이벤트를 디스패칭하는 핵심 과정입니다.
private var state: State = State.INITIALIZED
private fun moveToState(next: State) {
// 상태가 변경되지 않았으면 더 이상 진행하지 않음
if (state == next) {
return
}
// ...
// 변경된 상태로 업데이트
state = next
// ...
// 모든 등록된 Observer들에게 이벤트 전달 - Dispatch
sync()
}
moveState()에서는 전달받은 상태가 현재 상태와 비교하여, 다르다면 현재 상태를 업데이트합니다. 업데이트를 하고, sync()를 호출합니다. 해당 함수는 모든 등록된 관찰자들에게 상태가 변경되었다고 이벤트를 전달함을 아래 코드에서 확인해볼 수 있습니다.
private fun sync() {
val lifecycleOwner = lifecycleOwner.get()
?: throw IllegalStateException(
"LifecycleOwner of this LifecycleRegistry is already " +
"garbage collected. It is too late to change lifecycle state."
)
// 업데이트(디스패칭) 일어나지 않았는지 판단
while (!isSynced) {
newEventOccurred = false
// 현재 상태(state)와 모든 Observer이 가지고 있는 가장 오래된 상태를 비교
if (state < observerMap.eldest()!!.value.state) {
backwardPass(lifecycleOwner)
}
// 현재 상태(state)와 모든 Observer이 가지고 있는 가장 새로운 상태를 비교
val newest = observerMap.newest()
if (!newEventOccurred && newest != null && state > newest.value.state) {
forwardPass(lifecycleOwner)
}
}
newEventOccurred = false
_currentStateFlow.value = currentState
}
이 코드를 이해하기 위해서, Lifecycle의 State enum class에 정의되어 있는 순서를 다시 확인해 보겠습니다.
enum class State {
DESTROYED, *// 0*
INITIALIZED, *// 1*
CREATED, *// 2*
STARTED, *// 3*
RESUMED *// 4*
}
이 순서는 생물의 생명력의 정도로 비유할 수 있습니다. DESTROYED(0)가 가장 낮고, RESUMED(4)가 가장 높다고 볼 수 있습니다.
만약 현재 상태가 CREATED(2)인데 관찰자들 중 가장 오래된 상태가 STARTED(3)라면, 해당 관찰자의 상태를 CREATED(2)로 낮춰야 합니다. 이는 ON_STOP 이벤트를 디스패치함으로써 달성할 수 있습니다. 이 때 sync() 함수 내부에서는 backwardPass
메서드를 호출하고 있습니다.
private fun backwardPass(lifecycleOwner: LifecycleOwner) {
val descendingIterator = observerMap.descendingIterator()
while (descendingIterator.hasNext() && !isReentrance) {
val entry = descendingIterator.next()
val observer = entry.value
*// Observer의 상태가 목표 상태보다 높으면 낮춰야 함*
while (observer.state > state && observerMap.contains(entry.key)) {
*// STARTED(3) → CREATED(2)로 가려면 ON_STOP 이벤트 발생*
val event = Event.downFrom(observer.state) *// ON_STOP 반환*
observer.dispatchEvent(lifecycleOwner, event) *// ON_STOP 이벤트 전달!*
observer.state = event.targetState *// 상태를 CREATED(2)로 업데이트*
}
}
}
함수명으로 확인할 수 있듯 DESTROYED 방향으로 가도록 하는 디스패칭 작업을 Backward Pass라고 부릅니다. 자연에서 생물이 활발한 상태에서 쇠락을 거쳐 소멸로 향하는 과정과 같습니다.
Backward Pass에 해당되는 이벤트들을 정리하면 다음과 같습니다.
반대로 현재 상태가 STARTED(3)인데 관찰자들 중 가장 새로운 상태가 CREATED(2)라면, 해당 관찰자의 상태를 STARTED(3)로 높여야 합니다. 이는 ON_START 이벤트를 디스패치함으로써 달성할 수 있습니다.
private fun forwardPass(lifecycleOwner: LifecycleOwner) {
val iterator = observerMap.iteratorWithAdditions()
while (iterator.hasNext() && !isReentrance) {
val entry = iterator.next()
val observer = entry.value
*// Observer의 상태가 목표 상태보다 낮으면 높여야 함*
while (observer.state < state && observerMap.contains(entry.key)) {
*// CREATED(2) → STARTED(3)로 가려면 ON_START 이벤트 발생*
val event = Event.upFrom(observer.state) *// ON_START 반환*
observer.dispatchEvent(lifecycleOwner, event) *// ON_START 이벤트 전달!*
observer.state = event.targetState *// 상태를 STARTED(3)로 업데이트*
}
}
}
Forward Pass는 컴포넌트가 생명력을 증가시키는 디스패치를 의미합니다. 자연에서 생명체가 성장하여 활발히 상호작용할 수 있게 되는 과정과 같습니다.
결국 LifecycleRegistry에서 모든 관찰자들의 상태를 업데이트 하는 과정을 다음과 같이 정리해 볼 수 있습니다.
while (!isSynced)
루프로 완전히 동기화될 때까지 반복isSynced
체크로 모든 Observer의 상태가 일치함을 확인_currentStateFlow.value = currentState
로 최종 상태 반영이러한 양방향 Pass 시스템 덕분에 LifecycleOwner의 상태가 어떻게 변하든, 모든 Observer들이 항상 정확한 순서로 적절한 이벤트를 받을 수 있습니다.
이번 편에서는 안드로이드의 Lifecycle
과 그것을 보유하는 LifecycleOwner
, 그리고 Lifecycle
을 구현하여 생명주기 상태를 관리하고 상태 변화 이벤트를 전달하는 LifecycleRegistry
를 알아보았습니다.
다음 편에서는 생명주기 관찰자가 무엇인지, 그리고 그것이 필요한 이유를 살펴보도록 하겠습니다.
https://developer.android.com/topic/libraries/architecture/lifecycle?hl=ko
https://developer.android.com/static/images/topic/libraries/architecture/lifecycle-states.svg?hl=ko