[Android] 자연의 이치와 함께 이해해보는 안드로이드 생명주기 활용 (1)

Kame·2025년 8월 31일
0

Android

목록 보기
4/5
post-thumbnail

들어가며

이 시리즈에서는 안드로이드의 Lifecycle Architecture Component의 원리와 활용법을 알아봅니다.

모든 생명체는 생성부터 소멸까지의 과정을 겪습니다. 또한, 여러 생물들끼리는 자신만의 특성을 가지고 상호작용하면서 자신들이 존재하는 생태계에 변화를 일으키기도 합니다.

안드로이드 생태계에서도 자연환경과 크게 다르지 않은 모습을 볼 수 있습니다. 안드로이드 측에서 이러한 현상을 효율적으로 관리하기 위해 여러 수단들을 제공하는데, 이어질 내용에서 자세히 알아보도록 하겠습니다.


Lifecycle

생물들에게 수명이 주어지듯, 안드로이드의 컴포넌트 역시 수명 주기에 따라 동작하게 됩니다.

자연에서 생물은 시간의 흐름이라는 이벤트에 따라 탄생, 성장, 성숙, 노화, 소멸의 과정을 거치게 됩니다(관점에 따라 이벤트 및 과정은 다를 수 있습니다). 안드로이드 컴포넌트의 생명주기 또한 상태(States)를 가지며, 특정 이벤트(Events)에 의해 다른 상태로 전환됩니다. 바꿔 말하면 Activity나 Fragment, View 같은 컴포넌트들은 안드로이드의 Lifecycle에 따라 생성되고 활성화되며 최종적으로 소멸하는 과정을 거칩니다.

아래 그림은 안드로이드 공식 문서에서 제공하는 컴포넌트 생명 주기 다이어그램으로, 각 상태와 이벤트 간의 전환 과정을 시각적으로 보여줍니다.

State

안드로이드의 Lifecycle은 5가지 상태를 가집니다.

  • INITIALIZED : 새로운 생명체가 막 태어난 상태 → 컴포넌트가 막 생성되었지만, 아직 어떤 동작도 시작되지 않은 상태
  • CREATED : 생명 활동을 위한 기반이 마련된 상태 → 컴포넌트의 기본 구조와 리소스가 준비된 상태
  • STARTED : 주변을 둘러보고 감각을 활용할 수 있는 상태 → 컴포넌트가 화면에 보이기 시작하지만, 아직 사용자의 직접적인 상호작용은 제한적인 상태
  • RESUMED : 완전히 활성화되어, 모든 생명 활동(특히 상호작용)이 활발하게 이뤄질 수 있는 상태 → 컴포넌트가 포그라운드에서 사용자와 직접 상호작용할 수 있는 최상위 활성 상태
  • DESTROYED : 소멸을 맞이하여 더 이상 어떠한 생명 활동도 할 수 없는 상태 → 컴포넌트가 종료되고 모든 리소스가 해제된 상태

Event

안드로이드의 Lifecycle에서, 6가지 이벤트들이 상태 간의 전이를 유발합니다.

  • ON_CREATE : INITIALIZED → CREATED
  • ON_START : CREATED → STARTED
  • ON_RESUME : STARTED → RESUMED
  • ON_PAUSE : RESUMED → STARTED
  • ON_STOP : STARTED → CREATED
  • ON_DESTROY : CREATED → DESTROYED

추가적으로 ON_ANY라는 이벤트도 존재하는데, Lifecycle의 Event로 정의는 되어 있지만, 실제로는 프레임워크 내부에서만 사용되어야 하며 개발자가 직접 사용하면 안 되는 예약 이벤트입니다. 만약 개발자가 해당 이벤트를 발생시키는 것을 시도한다면 IllgalArgumentException 예외가 발생합니다(이벤트가 디스패치 되는 방식은 추후 설명합니다).

이러한 유한 상태 기계(FSM, Finite State Machine) 기반 구조에서 우리는 실제 자연 현상과의 유사성을 발견할 수 있습니다. 컴포넌트는 CREATED, STARTED 상태를 거쳐 가장 활발한 RESUMED 시기에 도달합니다. 이후 점차 쇠락하며 다시 STARTED, CREATED라는 기초 상태로 돌아갔다가, 마침내 DESTROYED에 이르게 되는데, 이는 생명체가 성장과 절정을 지나 쇠퇴와 소멸에 이르는 과정과 닮아 있습니다.

특징

Coroutines 활용 가능

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)
    • LifecycleOwner의 상태 변화에 따라 observer가 알림을 받을 수 있도록 등록합니다.
    • 등록 즉시 observer는 현재 Lifecycle 상태에 맞는 이벤트를 순차적으로 전달받습니다.
      • 예: LifecycleOwner가 STARTED 상태라면, observer는 먼저 ON_CREATE, 이어서 ON_START 이벤트를 받게 됩니다.
  • removeObserver(observer: LifecycleObserver)
    • 등록된 observer를 제거합니다.
    • 상태 변화 이벤트가 전달되는 도중에 호출되면, 이미 전달된 이벤트는 완료되고, 아직 전달되지 않은 이벤트는 전달되지 않습니다.
    • observer에 여러 메서드가 같은 이벤트를 감지하도록 되어 있을 경우, 하나라도 이벤트를 받았다면 나머지 메서드도 이벤트를 받고 그 이후에 제거됩니다.

결국 Lifecycle은 상태(State)와 이벤트(Event)를 기반으로 컴포넌트를 관찰 가능하게 만들어, 화면이나 앱 상태에 맞춰 자동으로 동작을 시작하고 중단할 수 있도록 하는 구조로 구현되어 있음을 알 수 있습니다.


LifecycleOwner

생명주기를 살펴보았으니, 이제 그 생명주기를 가지는 소유물을 살펴볼 수 있습니다.

안드로이드에는 LifecycleOwner라는 이름의 인터페이스가 존재하는데, 이름 그대로 Lifecycle 하나의 프로퍼티를 가지고 있는 모습입니다.

public interface LifecycleOwner {
    public val lifecycle: Lifecycle
}

이 간단해 보이는 인터페이스가 사실은 전체 생명주기 시스템의 핵심입니다. 모든 생물이 '생명'이라는 공통된 특성을 가지는 것처럼, 모든 LifecycleOwner는 자신만의 Lifecycle 인스턴스를 소유합니다.

대표적으로 우리가 사용하는 ActivityFragment는 모두 이 인터페이스를 구현하고 있습니다. 이 두 가지 컴포넌트는 자신의 Lifecycle을 가지고 있으며, 생명주기 변화에 따라 우리에게 너무나도 익숙한 콜백(onCreate, onStart, onResume 등)의 동작을 정의하는 것이 일반적입니다.

즉, LifecycleOwner자신의 상태(State)를 관리하고, 상태 변화에 따라 Observer에게 알림을 전달하는 역할을 하며, 이를 통해 앱 내 다양한 컴포넌트가 생명주기에 안전하게 반응할 수 있도록 합니다.

이 인터페이스를 활용하면 Activity, Fragment뿐만 아니라 커스텀 컴포넌트도 자신의 생명주기를 정의하고, Lifecycle에 종속적인 동작을 구현할 수 있는데, 관련 내용은 이후 살펴보도록 하겠습니다.


LifecycleRegistry

레지스트리(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

정의

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)가 가장 높다고 볼 수 있습니다.

⬅️ Backward Pass - 소멸을 향하다

만약 현재 상태가 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에 해당되는 이벤트들을 정리하면 다음과 같습니다.

  • ON_PAUSE: RESUMED(4) → STARTED(3) - 일시적 비활성화
  • ON_STOP: STARTED(3) → CREATED(2) - 외부와의 소통 차단
  • ON_DESTROY: CREATED(2) → DESTROYED(0) - 완전한 소멸

➡️ Forward 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는 컴포넌트가 생명력을 증가시키는 디스패치를 의미합니다. 자연에서 생명체가 성장하여 활발히 상호작용할 수 있게 되는 과정과 같습니다.

  • ON_CREATE: INITIALIZED(1) → CREATED(2) - 기본 생명 구조 형성
  • ON_START: CREATED(2) → STARTED(3) - 외부 환경 인식 시작
  • ON_RESUME: STARTED(3) → RESUMED(4) - 완전한 활성화

결국 LifecycleRegistry에서 모든 관찰자들의 상태를 업데이트 하는 과정을 다음과 같이 정리해 볼 수 있습니다.

  1. moveToState(새로운상태) 호출
    • LifecycleOwner(예: Activity)의 상태가 변경됨
    • 내부 state 필드를 새로운 상태로 업데이트
  2. sync() 실행
    • 모든 등록된 Observer들과 상태 동기화 시작
    • while (!isSynced) 루프로 완전히 동기화될 때까지 반복
  3. 가장 오래된 Observer와 현재 상태 비교
    • 현재 상태가 더 낮으면, Observer들을 낮은 상태로 끌어내림
    • 예: CREATED(2) < STARTED(3) → ON_STOP 이벤트 발생
  4. 가장 최신 Observer와 현재 상태 비교
    • 현재 상태가 더 높으면, Observer들을 높은 상태로 끌어올림
    • 예: STARTED(3) > CREATED(2) → ON_START 이벤트 발생
  5. 모든 Observer 동기화 완료
    • 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

profile
Software Engineer

0개의 댓글