안드로이드 Android

안드로이드 개발 (46) Process, Activity, Compose LifeCycle

SEOBI서비 2025. 5. 2. 00:05

안드로이드 개발에서 Process, Activity, Compose 의 세가지 LifeCycle은 안드로이드 개발자가 알아야할 필수 지식이며, 앱의 문제 해결에 있어서 매우 중요한 주제 입니다. 특히 실무 개발자라면 각 컴포넌트를 생성하고 파괴하는 과정과 그 사이에서 적절히 동작을 관리하는 방법을 잘 이해해야합니다.

1. 안드로이드 앱 생명주기 (Application LifeCycle)

//Application Class
import android.app.Application

class CustomApplication: Application(){
    override fun onCreate() {
        super.onCreate()
        //.. 앱 초기화 시점 로직 실행..
    }
}


Application 클래스는 애플리케이션 전역에서 한 번 생성되는 객체로 앱의 전체 수명주기를 관리하는 역할을 합니다. 앱 프로세스가 시작될 때 가장 먼저 생성되며 앱이 종료될 때까지 존재하면서 전역 상태를 유지합니다. Application 클래스를 정의하면 앱 초기화 로직이나 전역 리소스 관리를 한 곳에 모아둘 수 있어 편리합니다.

Application 의 역할

  • 글로벌 상태 관리: Application 객체는 애플리케이션 전역에서 참조 가능한 Context를 제공합니다. 이를 통해 다양한 컴포넌트(액티비티, 서비스 등)에서 공용으로 사용할 데이터나 자원을 관리할 수 있습니다.
  • 초기화 작업: Application 의 onCreate() 는 앱이 시작될 때 한 번 호출되므로, 전역 초기화 작업을 수행하기에 적합합니다. 서드파티 라이브러리 초기화, DI 컨테이너 세팅 등을 여기서 진행합니다.
  • 공용 리소스 제공: Application 은 ContextWrapper 를 상속 하므로 Context가 필요한 작업을 어디서든 수행할 수 있습니다. 그래서 Activity Context 를 사용하지 않는 사용처에서도 Application Context를 통해 리소스 로드나 시스템 서비스 사용 등을 작업할 수 있습니다.

 Application 주요 메서드

class CustomApplication: Application(){
    override fun onCreate() { }
    override fun onTerminate() { }
    override fun onLowMemory() { }
    override fun onConfigurationChanged(newConfig: Configuration) { }
	override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        when(level){
            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN ->...
            else -> ...
        }
    }

}
  • onCreate() - 앱의 프로세스가 생성될 때 호출되며, 가장 먼저 실행됩니다. 앱의 전반적인 LifeCycle 에서 딱 한번만 호출됩니다. (멀티프로세스 환경이라면 각 프로세스마다 한 번씩 호출됨)
  • onTerminate() - 앱 종료시 호출됩니다. 하지만 이 메서드는 실제 단말기에서는 호출되지 않을 수 있습니다. 안드로이드 시스템은 일반적으로 프로세스를 강제로 종료하므로 onTermiante() 가 호출되지 않을 수 있습니다.  

  • onConfigurationChanged(newConfig: Configuration) - 앱의 구성 변경 시 호출됩니다. 구성 변경은 언어, 다크모드/라이트모드 변경, 화면 방향 등으로 인해 발생하는 환경 변경입니다. 현대 안드로이드 앱 개발에서 다크모드 <-> 라이트모드 변경에 따라 콜백 세팅을 수행하기 위해 사용하는 사례가 많습니다.

  • onLowMemory() -  전체 시스템 메모리가 부족할  때 호출됩니다. Android 4.0 이전에는 이 콜백을 통해 메모리 해제를 수동 처리했으나, 이후에는 더 세분화된 onTrimMemory() 가 도입되었습니다. 따라서 Android 4.0 이상에는 이 메서드가 잘 호출되지 않으며, 하위 호환을 위해 남겨둔 fallback 정도로 간주합니다.

  • onTrimMemory(level:Int) - Android 4.0 이상부터 도입된 메서드로 메모리 트림 요청(trim memory)로 인해 호출이 됩니다. level를 android.content.ComponentCallbacks2의 상수값과 비교해서 메모리 상태에 상황에 따라 다양한 작업을 수행 할 수 있습니다. 
    -> 메모리 트림 요청이란? 안드로이드 시스템이 "지금 메모리가 필요하니 어느정도 반납해달라" 라고 신호를 보내는 콜백입니다.
    -> ComponentCallbacks2 의 상수값 예시
public interface ComponentCallbacks2 extends ComponentCallbacks {
    //멀티-윈도 / 픽처-인-픽처 일부 가려짐
    int TRIM_MEMORY_BACKGROUND = 40;

	// 앱이 백그라운드인데 시스템 메모리가 계속 부족
    int TRIM_MEMORY_COMPLETE = 80;
    int TRIM_MEMORY_MODERATE = 60;

    // 시스템이 전체적으로 메모리 여유가 부족
    int TRIM_MEMORY_RUNNING_CRITICAL = 15;
    int TRIM_MEMORY_RUNNING_LOW = 10;
    int TRIM_MEMORY_RUNNING_MODERATE = 5;

    // 앱 UI가 화면에서 완전히 사라짐
    int TRIM_MEMORY_UI_HIDDEN = 20;

    void onTrimMemory(int var1);
}

 

 

또한, Application 클래스 자체에 ComponenetCallbacks2 인터페이스를 구현하고 있기 때문에, Application의 onTrimMemory를 사용하지 않고도 registerComponentCallbacks { } 를 통해 별도 콜백을 등록해 메모리 이벤트를 수신할 수 있습니다.

package android.app;

//ComponentCallbacks2 인터페이스를 구현한다.
public class Application extends ContextWrapper implements ComponentCallbacks2 { .. }

 

class CustomApplication : Application() {

    // ComponentCallbacks2 구현체를 람다로 간단히 준비
    private val memoryCallback = object : ComponentCallbacks2 {
        override fun onTrimMemory(level: Int) {
            // 필요에 따라 캐시 비우기, 리소스 해제
            if (level >= TRIM_MEMORY_BACKGROUND) {
                ImageCache.clear()
            }
        }
		//..기타 오버라이드 코드 생략..//
    }
	
    override fun onCreate() {
        super.onCreate()
        // 콜백 등록
        registerComponentCallbacks(memoryCallback)
    }

}

 

 

그리고 Application class의 추가 팁이 있습니다. 바로 registerActivityLifecycleCallbacks 를 사용하면 앱 내의 각 Activity LifeCycle 이벤트를 Application에서 전역적으로 모니터링 하거나 세팅이 가능합니다.  

class CustomApplication : Application() {

    private val callbacks = object : ActivityLifecycleCallbacks {
        override fun onActivityCreated(a: Activity, b: Bundle?) {}
        override fun onActivityStarted(a: Activity) {}
        override fun onActivityResumed(a: Activity) {}
        override fun onActivityPaused(a: Activity) {}
        override fun onActivityStopped(a: Activity) {}
        override fun onActivitySaveInstanceState(a: Activity, out: Bundle) {}
        override fun onActivityDestroyed(a: Activity) {}
    }

    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(callbacks)
    }

    override fun onTerminate() {
        unregisterActivityLifecycleCallbacks(callbacks)
        super.onTerminate()
    }
}

 

 

팁: 전역 예외 처리 Thread.setDefaultUncaughtExceptionHandler를 Application에서 설정하여 전역 예외를 감지하고 처리(로그 전송 등)하는 패턴도 있습니다.

class CrashHandler(
    private val context: Context,
    private val delegate: Thread.UncaughtExceptionHandler? = null
) : Thread.UncaughtExceptionHandler {

  	override fun uncaughtException(thread: Thread, throwable: Throwable) {
  	//...
  	}
    
}

class CustomApplication : Application() {

    override fun onCreate() {
        super.onCreate()
      
        val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()

        // 새 핸들러 등록
        Thread.setDefaultUncaughtExceptionHandler(
            CrashHandler(this, defaultHandler)
        )
    }
}

 

2.  안드로이드 프로세스 생명주기와 메모리 관리

안드로이드에서는 앱의 프로세스 생명주기가 앱 자체가 아닌  시스템에 의해 관리 됩니다. 안드로이드 시스템은 앱의 컴포넌트 활용 상태 및 현재 메모리 상황 등을 종합적으로 고려하여 프로세스를 생성하거나 종료합니다. 일반적으로 각 앱은 별도의 Linux 프로세스로 격리되어 실행되고, 해당 앱의 코드가 실행되어야 할 때(예: 액티비티 시작) 필요에 따라 프로세스가 생성 됩니다. 한 번 생성된 프로세스는 사용자가 앱을 계속 사용하거나 백그라운드에 남아 있는 한 유지되지만, 시스템 메모리가 부족해지만 더 중요한 작업을 수행중인 다른 앱에 메모리를 양보하기 위해 불필요한 프로세스를 종료시킵니다. 

프로세스 생성 시점 

  • 홈 화면에서 앱을 실행: 해당 앱에 대한 새로운 프로세스가 Zygote로 부터 fork 되어 생성됩니다. 이 시점에 Application.onCreate가 호출됩니다. 새로운 프로세스를 완전히 생성하지 않고 이미 실행중인 씨앗 프로세스 Zygote로 부터 복제를 하기 때문에 새로운 프로세스를 빠르게 만들어 낼 수 있다고 합니다.

  • 백그라운드 서비스 시작: 앱의 서비스가 예약(AlarmManager, WorkManager 등)되어 있거나 백그라운드에서 동작을 시작해야 하는 경우, 기존 앱 프로세스가 살아 있다면 이를 재사용하고, 이미 종료된 상태라면 새로운 프로세스를 생성합니다. 두 경우 모두 프로세스가 처음 시작될 때는 Application.onCreate()가 호출된 후 서비스 컴포넌트의 시작 메서드(Service.onCreate())가 이어서 호출됩니다.

  • BroadcastReceiver 실행: 예를 들어 BOOT_COMPLETED 이나 특정 이벤트를 수신하도록 등록된 리시버가 있다면, 해당 인텐트를 기존 앱 프로세스가 살아 있다면 이를 재사용하고, 이미 종료된 상태라면 새로운 프로세스를 생성합니다.  두 경우 모두 프로세스가 처음 시작될 때는 Application.onCreate()가 호출된 후 Receiver.onReceive() 가 이어서 호출됩니다.

메모리 부족과 프로세스 종료 (OOM Kill)

시스템 메모리가 부족해지면, 안드로이드 시스템은 어떤 프로세스를 종료할지 결정하기 위해 프로세스의 중요도에 우선 순위 정리(LRU) 목록을 관리합니다. 중요도가 낮은 프로세스부터 차례로 종료하여 메모리를 회수하고, 꼭 필요한 프로세스는 최후까지 유지하는 전략입니다. 프로세스의 중요도 레벨과 특징은 아래와 같습니다. 

 

프로세스 유형 설명 (중요도 높은 순) 메모리 정리 순위
포그라운드 프로세스
(Foreground)
사용자가 현재 직접 상호작용 중인 컴포넌트를 포함하는 프로세스 입니다. 예를들어 전체 화면이보이는 액티비티(onResume()) , 현재 실행 중인 포그라운드 서비스(startForeground()) 또는 실행중인 리시버(onReceive() 리턴 전) 등을 포함 합니다.  최우선 유지 - 시스템이 극한의 메모리 부족 상태에 이르지 않는 한 종료되지 않습니다.
가시적 프로세스
(Visible)
사용자에게 화면상 보이지만 전체화면이 아닌 컴포넌트를 포함하는 프로세스 입니다. 예를들어 투명 액티비티나 다이얼로그 뒤에 보이는 액티비티(onPause()), 화면에 보이는 위젯 등이 있습니다.  매우 중요 - 포그라운드 상태 다음으로 중요하며, 메모리가 부족하면 다른 모든 캐시 프로세스를 종료해도 메모리가 모자를 때 종료됩니다.
서비스 프로세스
(Service)
백그라운드 서비스를 실행중인 프로세스 입니다. 직접 UI를 보이진 않지만 사용자가 신경쓰는 동작을 수행중입니다. (예: 음악 재생, 데이터 동기화 등) 중간 - 사용자가 인지하는 작업이므로 함부로 종료되지 않지만, 프로세스/가시적 상태를 유지 후에도 메모리가 부족하면 캐시 프로세스 다음으로 종료됩니다. 또한 장기간 실행 된 서비스는 캐시프로세스로 강등 되기도 합니다.
캐시 프로세스
(Cached)
현재 필요하지 않은 프로세스들로 백그라운드에 대기 상태로 있는 앱들 입니다. 화면에 나타나는 컴포넌트가 없고 바인드된 서비스 등도 없는 프로세스가 이에 해당합니다. 시스템 입장에서 언제든 제거해도 무방한 프로세스 입니다. 최우선 정리 - 시스템은 메모리가 부족해지면 언제든지 프로세스를 종료하여 메모리를 확보합니다.
최신 안드로이드 기기에서는 주로 메모리를 가장 많이 사용하는 프로세스부터 정리하며, 그렇지 않은 경우에는 오랫동안 사용되지 않은 순서(LRU)로 캐시 프로세스를 종료합니다.

 

쉽게 설명하면 실제 사용자 눈에 UI가 눈으로 들어올 수록 가장 메모리 유지 가능성이 높고 UI 다음은 백그라운드 작업이고 마지막에는 사용되지 않는 프로세스 순으로 메모리 정리 우선 순위가 결정됩니다.

 

OOM(Out-Of-Memory)과 메모리 모니터링

안드로이드 시스템은 Low Memory Kiler(LMK) 라는 메커니즘이 있습니다. 메모리 부족이 감지되면 위에 언급한 중요도 순서에 따라 자동으로 프로세스를 정리합니다. 개발 중에는 쉘을 통해 메모리 사용량을 확인하고, 어플리케이션 or ComponentCallbacks의  onTrimMemory(level:Int) 콜백을 활용하여 메모리 해제 로직이 잘 작동하는지 테스트 해볼 수 있습니다. 

//메모리 확인 
adb shell dumpsys meminfo

 

특히 onTrimMemory(level:Int) 콜백에 level 파라미터가 TRIM_MEMORY_UI_HIDDEN 로 호출될 때 앱이 차지하는 메모리를 크게 줄여두면 시스템 메모리 관리에 협조적이고 앱이 백그라운드에서 오래 살아 남을 가능성이 높아집니다. 

 

- onTrimMemory(TRIM_MEMORY_UI_HIDDEN) 가 올때 메모리를 줄이면 앱이 오래 살아 남는 이유?

┌ 홈 버튼
│
├ onPause()       ← 사용자에게 안 보임
├ onStop()
└ onTrimMemory(TRIM_MEMORY_UI_HIDDEN)   ← 바로 여기!

 

호출 시점-—“UI 안 보여, 이제  캐시 취급할게!”

  • TRIM_MEMORY_UI_HIDDEN 은 액티비티 화면이 전부 사라진 직후(예: 홈 버튼을 눌러 다른 앱으로 전환) 한번 호출됩니다.
  • 프로세스 유형 표에 따라 백그라운드 작업이 수행 중이지 않으면 캐시 프로세스로 전환되서 언제든지 시스템의 메모리 반환 요청으로 인해 정리 될 수 있습니다.
  • 캐시 프로세스로 진입할 때 메모리 사용률을 최대한 줄인다면 LMK 에 의해 프로세스가 제거 되지 않도록 제거 대상 순위를 내릴 수 있습니다. 즉, 당장 화면에 필요 없는 리소스(불필요 서비스/리시버, WebView , Bitmap 캐시, OpenGL 텍스처, View 트리, 미디어 객체 등)를 정리를 해서 메모리 사용량을 확 줄이는 겁니다.
override fun onTrimMemory(level: Int) {
    super.onTrimMemory(level)
    if (level == TRIM_MEMORY_UI_HIDDEN) {

        // 1) 이미지 로더 캐시 줄이기
        Glide.get(this).trimMemory(TRIM_MEMORY_UI_HIDDEN)

        // 2) LruCache 비우기
        myBitmapCache.evictAll()

        // 3) 비활성 WebView 파괴
        webView?.run {
            loadUrl("about:blank")
            clearHistory(); destroy(); webView = null
        }
        
        // 4) 기타 작업 등등
    }
}

 

멀티 프로세스 

일반적으로 하나의 앱은 하나의 프로세스에서 실행됩니다. 하지만 프로세스 분리의 필요성을 느끼면 멀티 프로세스 분리해서 사용이 가능합니다. 안드로이드에서는 AndroidManifest.xml 에서  android:process 속성을 활용해서 액티비티,서비스,리시버,프로바이더에 각 핵심 컴포넌트에 별도 프로세스 실행할 수 있도록 지정할 수 있습니다. 

<!-- Main 앱 프로세스 -->
<activity android:name=".MainActivity"/>

<!-- 별도 프로세스로 실행되는 서비스 -->
<service
    android:name=".BackgroundService"
    android:process=":background"/>

     

멀티 프로세스 구조의 특징은 프로세스 마다 별개의 Application 객체가 생성이 된다는 것입니다.  프로세스 특징상 서로 다른 메모리를 공유하지 않으므로 스태틱이나 전역 변수등도 프로세스마다 별도로 존재합니다. 

 

android:process 사용 시 주의사항

  1. 필요한 경우에만 사용하기: 멀티 프로세스는 앱 구조를 복잡하게 만들고 앱 자체의 메모리 사용량을 대폭 늘리므로 사용 여부를 잘 생각각 해야합니다. 이유 없이 단순 목적으로 프로세스를 나누면 메모리 사용량만 대폭 늘어 안드로이드 시스템의 LMK의 활동이 빈번해집니다.

  2. 프로세스 간 통신 고려 : 분리된 프로세스 간에는 메모리를 공유하지 않으므로 데이터 공유가 필요하면 파일, DB, ContentProvider 또는 Binder(AIDL) 등을 사용해야 합니다. 단순히 전역 변수를 쓰는 것이 불가능 합니다.

  3. Application 초기화 분기 처리: 가장 중요한 점으로 Application.onCreate() 내에서 현재 프로세스가 어떤 프로세스 인지 확인하고 초기화 작업을 잘 생각해서 진행 해야합니다. 예를들어 UI 관련된 초기화는 메인 프로세스만 하면 되는데 백그라운드 관련 프로세스에서 진행을 하게되면  불필요한 작업을 해버리는 것입니다.

아래와 같이 프로세스 이름을 확인해서 분기 처리가 가능합니다.

class CustomApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // 현재 프로세스 이름 가져오기 (API 28+ 간단하게)
        val processName = Application.getProcessName() ?: run {
            // API 28 미만 호환 방법: RunningAppProcessInfo에서 찾기
            val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
            am.runningAppProcesses?.find { it.pid == android.os.Process.myPid() }?.processName
        }
        
        // 메인 프로세스가 아닐 경우 조기 반환하여 불필요한 초기화 막기
        if (processName != null && processName != packageName) {
            return  
        }
        
        // 이 아래는 메인 프로세스에서만 실행할 초기화 코드들
        initLibraries()    
        setupGlobalResources()
    }
}

 

 

멀티 프로세스의 장단점

멀티 프로세스의 특징과 장단점을 잘 생각해서 앱에 적용할지 잘 생각 해야합니다. 다양한 장단점이 있지만 매우 큰 3가지를 정리했습니다. 아래와 같습니다.

구분 장점 단점
안전성 및 격리 - 한 프로세스 OOM 및 Crash가 발생하더라도 다른 프로세스는 그대로 생존
(예:서비스 전용 프로세스가 제거되어도 UI전용 프로세스는 살아있어서 앱 UI는 유지됨 즉, 사용자에게 안정적인 앱이라고 느끼게끔 할수도 있음)  

-  보안 취약 코드 및 플러그인을 안전하게 격리
- 컴포넌트가 서로 죽었다 살아날 때 상태 및 세션 유지 코드 필요 (예: A 프로세스에서 B프로세스의 Bind Serive 의존 할때 B프로세스 사망시 재바인드 필요 등)
메모리 한도 우회 - 프로세스 단위 별로 메모리 힙 상한 값이 적용이 되어있음, 하지만 멀티 프로세스를 사용하면 하나의 프로세스 보다 더 많은 메모리 힙을 각각 사용할 수 있게 됌
(예: 프로세스 하나당 512mb 가 주어진다면, 서비스에서 무거운 작업을 한다했을때 액티비티 전용 프로세스와 서비스 전용 프로세스를 분리하면 서비스 전용 프로세스에서 512mb 사용 가능)
- 한 패키지 단위 앱의 총 RAM 사용량이 증가하므로 안드로이드 시스템 자체의 메모리 사용율 급증으로 인해서 LMK 활동이 매우 많아집니다. (예: 안드로이드 시스템 자체의 메모리 부족으로 앱 버벅임, 갑자기 음악재생 종료 및 다른 어플 종료 등)
업데이트 및 모듈화  - 프로세스 경계가 API 역활을 해버림 즉 팀별 독립 개발 및 버전 차등 적용하기에 편리함 - 실시간 프로세스간 통신이 필요할때 IPC를 사용해야할때, 꾸준한 인터페이스 유지보수 필요합니다. 

 

멀티 프로세스 사용 실제 사례

대표적인 예시로 카카오톡 어플이 있습니다. 카카오톡은 멀티 프로세스 구조를 사용하고 있는데 메인 UI 전용 프로세스와 푸시 서비스전용 프로세스가 함께 존재합니다. (그외에도 있지만 언급은 하지 않겠습니다.) UI 전용 프로세스가 죽어도 푸시 서비스 전용 프로세스로 인해서 메인 UI 전용 프로세스가 죽어도 푸시 서비스 로직이 잘 유지될 수 있도록 합니다. 그래서 푸시 서비스에 관한 OOM 및 crush 을 잘 유지하도록 한 것입니다.

- 카카오톡도 푸시도 FCM으로 보낼텐데 왜 굳이 분리 프로세스를 했을까? 

FCM 은 내 앱이 죽어도 데이터 메시지에 저장해 둡니다. 하지만 바로 실행되지 않을 수 있으며, 엄격한 Doze 제한이 걸린 기기는 오랜 지연이 걸릴 수있습니다. 즉, 상용 레벨에서 요구하는 신뢰성을 만족하려면 별도의 프로세스를 유지해서 빠른 실시간성 및 소켓 및 알림은 살아 있도록 컨트롤 했어야 할겁니다.   

 

프로세스 추적 및 디버깅 방법

앱의 프로세스 상태를 추적하고 생명주기 이벤트를 확인하면 많은 도움이 됩니다. 안드로이드에서는 ADB 명령어나 로그를 통해 프로세스 정보를 확인 할 수 있습니다. 현재 실행 중인 프로세스 나열 (abs shell ps): ADB 쉘에서 ps 명령으로 현재 실행중인 모든 프로세스를 볼 수 있습니다.

$ adb shell ps | grep com.example.myapp
u0_a123   10932  ...  com.example.myapp
u0_a123   11389  ...  com.example.myapp:myservice

 

위 출력 예시에서, com.example.myapp (메인 프로세스)와 com.example.myapp:myservice (별도 서비스 프로세스) 두 개가 실행 중임을 알 수 있습니다.

 

3.  액티비티 생명주기 (Activity Lifecycle)

Activity는 안드로이드 앱의 사용자와 상호 작용할 수 있는 진입 지점 입니다. (단순 UI 화면으로 보면 안된다 생각합니다.) 액티비티 생명주기는 앱이 상호작용하는 동안 화면 전환, 일시정지, 재개, 종료 등의 과정을 관리합니다. 

 

  • onCreate() - 액티비티 생성 초기화 단계 입니다. UI는 xml 및 View class 사용시에는 setContnetView() 를 설정해서 사용하고 Compose 사용 시  setContent {} 를 사용합니다.  savedInstanceState 를 통해 프로세스가 종료 시키기 이전 상태 복원도 가능합니다만 적은 용량만 가능한 한계가 있습니다. Compose 의 경우 rememberSaveable 를 통해 번들에 저장할 수 있습니다. aac ViewModel을 쓴다면 SavedStateHandle을 통해서 번들 값을 가져올 수 있습니다.

  • onStart() - 액티비티가 화면에 보이기 시작하는 단계입니다. 아직 포커스를 갖지 않은 상태지만 사용자에게 UI가 보이게 되는 위치입니다. 이 단계에서는 UI를 갱신하거나 필요한 리소스를 확보하는 작업을 합니다.

  • onResume() - 액티비티가 포그라운드가 되어 사용자와 상호작용하기 바로 직전 호출됩니다. 이호출이 끝나면 액티비티는 화면에서 포커스를 얻고 사용자 입력을 받을 수 있게 됩니다. 

  • onPause() - 액티비티가 다른 화면에 일부 가려지거나 포커스를 잃을 때 호출 됩니다. 예를 들어, 반투명한 새로운 액티비티가 뜨거나 현재 액티비티 위에 다이얼로그 창이 뜬 경우 입니다. 이 단계에서는 사용자 입력을 바로 받을 수 없습니다. (실무적인 관점에서 봤을때 UI 관련 리스너 등록을 이 단계에서 해제 하는것이 안전했습니다. )

  • onStop() - 액티비티가 완전히 화면에서 사라질 때 호출됩니다. 사용자가 다른 액티비티로 완전히 넘어가거나 홈 화면으로 나가는 경우 등 UI가 더이상 보이지 않게 된 상태입니다. 

  • onReStart() - 기존 액티비티가 onStop() 이었다가 다시 재개(Resume) 되었을 경우 호출 됩니다.

  • onDestory() - 액티비티가 종료되어 소멸되기 직전에 호출됩니다. finish() 를 통해 액티비티를 종료하거나 이전 액티비티로 돌아가기 위해 뒤로가기 버튼을 누르면 돌아가기전 액티비티의 onDestory()가 호출되기도 합니다. (시스템이 프로세스를 바로 죽여버리는 경우 onDestory()가 호출되지 않을 수 잇습니다. )

1️⃣ A → B 로 화면 전환

A.onPause()        // A는 더 이상 입력 포커스를 갖지 않음
B.onCreate()
B.onStart()
B.onResume()       // B가 화면·포커스 모두 갖춤
A.onStop()         // A는 완전히 가려져 보이지 않음

 

  • A.onPause() - A.onStop() 사이에 오래 걸리는 I/O를 메인 스레드에서 수행 하면 UI 지연이 발생 합니다. 
  • A가 singleInstance 등이 아니라면 프로세스 안에 그대로 남아 있으며, 메모리가 부족해야만 시스템이 A.onDestroy() 를 호출 합니다.

2️⃣ B → 뒤로가기 (Back) → A 복귀

B.onPause()        // B는 포커스 상실
A.onRestart()      // 멈춰 있던 A가 다시 화면에 드러날 준비
A.onStart()
A.onResume()       // A가 다시 포커스 획득
B.onStop()
B.onDestroy()      // 기본 back stack 정책: B 인스턴스 제거
  • A.onResume() 이 호출이 되야만 B.onStop() 이 호출이 됩니다.
  • A 액티비티가 onStop() 상태로 있었다가 B 액티비티의 뒤로가기로 인해 A가 다시 재개되서 onRestart() 가 호출됩니다. 즉, onRestart() 는 onStop() 에서 다시 재개 되어야만 호출 됩니다.

Jetpack Lifecycle로 생명주기 관리 하는법

Jetpack Lifecycle가 제공하는 세 가지 API를 통해 다양하게 생명주기를 관리 할 수 있습니다.

dependencies {
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.6"
}


1️⃣ 클래스 단위로 관리 — DefaultLifecycleObserver

원하는 생명주기 메서드를 override 해서 사용할 수 있습니다. 클래스 단위로 로직을 분리 할 수 있어서 Activity의 가독성을 해치지 않습니다.  

class LifecycleObserverTest(
    context: Context
) : DefaultLifecycleObserver {                       
    override fun onResume(owner: LifecycleOwner) {   
        //..
    }
    override fun onPause(owner: LifecycleOwner) {
        //..
    }
}

class MainActivity : AppCompatActivity() {
    override fun onCreate(..) {
        //..
        lifecycle.addObserver(LifecycleObserverTest(this))
    }
}

 


2️⃣ 람다 한 줄로 끝 — LifecycleEventObserver

람다로 심플하게 설정할 수 있고 람다안에서 원하는 생명주기를 분기처리 할 수 있습니다. (Lifecycle.Event... 사용)

val quickLog = LifecycleEventObserver { _, event ->
    if (event == Lifecycle.Event.ON_CREATE) {
        Log.d("Lifecycle", "Activity 방금 생성됨")
    }
}
lifecycle.addObserver(quickLog)


3️⃣ 코루틴·Flow와 함께 — repeatOnLifecycle
코루틴 Flow 를 이용해서 가능합니다. 특정 라이프 사이클에 맞게 Flow collect을 시작합니다. 아래 예제는 STARTED (onStart()) ~ STOPPED(onStop()) 사이에서 사용됩니다. STARTED 상황 일때 블록이 재실행 되고 STOPPED 이면 자동으로 취소 됩니다. 

lifecycleScope.launch {
// STARTED~STOPPED 사이만 실행
    repeatOnLifecycle(Lifecycle.State.STARTED) {     
        viewModel.uiState.collect { state ->
       		//...
        }
    }
}

 

4️⃣ Compose 에서의 처리  — LifecycleEventEffect, LifecycleResumeEffect, LifecycleStartEffect

Composable 에서 쉽게 API 를 통해 라이프 사이클을 관리할 수 있습니다. 이 부분은 본문 4. Compose LifeCycle 관리에서 다뤄보도록 하겠습니다.

상태 저장과 복원 (onSaveInstanceState & onRestoreInstansceState)

액티비티는 일시적으로 파괴되었다가 (onDestory()) 나중에 다시 생성될 수 있습니다. 대표적인 경우가 화면 회전이며 이때 기존 액티비티는 파괴되고 새로운 액티비티 인스턴스가 생성됩니다.  이 과정에서 UI 상태가 모두 사라집니다. 이를 위해 별도의 상태저장/복원이 필요합니다.

 

onSaveInstanceState() – 시스템이 액티비티를 파괴해야 할 때를 대비해서 앱이 백그라운드 상태에 진입하면 현재 상태를 Bundle에 저장합니다. 기본 View 계층의 상태(입력된 텍스트, 체크박스 체크 여부 등)를 자동 저장합니다. (컴포즈는 예외 입니다.) Custom View 나 별도의 상태는 이 메서드에서 outState.putXXX 형태로 직접 저장해야 합니다. (컴포즈는 rememberSaveable 사용) 이 메서드는 Android 8.1 이하에서는 onPause() Android 8.1 이상은 onStop() 이후에 호출 됩니다. 

OS 버전  전형적 호출 순서  왜 이렇게?
API 1 – 27 (Android 8.1 Oreo↓) onPause() ⇒ onSaveInstanceState() ⇒ onStop() 화면이 아직 Stopped 되기 전에 UI 트리에 접근할 기회를 주기 위해
API 28 + (Android 9 Pie↑) onPause() ⇒ onStop() ⇒ onSaveInstanceState() 최신 다중-윈도우/제스처 전환 최적화. UI 가 이미 Stopped 상태라서 “UI 수정 금지 구간”이 짧아짐. oaicite:0

 

시나리오 onSaveInstanceState() 발생 여부
홈 버튼 / 최근 앱으로 나가기 O (대부분)
회전·다크모드 등 구성 변경 O
멀티 윈도우 전환 O
finish() 호출 / Back 버튼으로 완전 종료 X (돌아올 가능성이 없다고 판단) oaicite:1
시스템이 kill 하기 직전 이미 호출돼 있었어야 함 (직전 호출 X)

 

onRestoreInstanceState() - 저장된 상태가 있을 경우 onStart() 직후에 호출 되어 onSaveInstanaceState 에 넣어둔 값을 꺼내와 뷰나 변수에 복원합니다. 이 메서드를 구현하지 않더라도 onCreate()의 savedInstanceState 파라미터를 통해 같은 Bundle을 받을 수 있도 있습니다. 일반적으로는 onCreate()에서 상태를 복원하거나 필요한 경우에만 onRestoreInstanceState를 별도로 오버라이드합니다.  (compose 같은 경우 rememberSaveable 에 있어서 개발자가 세팅한대로 UI와 상태가 자동 매칭 됩니다.)

 

4.  Compose  LifeCycle 관리 

Composable 에서 Activity Lifecycle 관리법

현대 안드로이드 앱개발에서 Jetpack Compose 를 사용시 Activity = 셋업 + 네비게이션 호스트만 두고, 컴포저블은 Side effect API 로 컴포저블 내에서 자신의 수명에 맞춰 일을 하고 정리합니다.  이 조합이면 Activity 의 onStart() 및 onStop() 콜백을 직접 건들일은 없습니다.

API  언제 쓰나  특징
LifecycleEventEffect(Lifecycle.Event) 특정 라이프 사이클 이벤트에 맞춰 side-effect 실행 LifecycleEventEffect(ON_START){ … }
LifecycleStartEffect / LifecycleResumeEffect (ON_START↔ON_STOP / ON_RESUME↔ON_PAUSE)을 다룰 때 시작 블록 + onStopOrDispose{} 정리 블록 필수
DisposableEffect(key) 컴포저블 진입/탈출 시 초기화·해제 코드를 한쌍으로 전통적인 “add listener → remove listener” 자리
     

 

LifecycleEventEffect(Lifecycle.Event)

특정 라이프 사이클이 호출될 때 마다 composable 내에서 작업을 수행 하도록 합니다. Lifecycle.Event 의 상태를 파라미터로 넘겨서 원하는 라이프 사이클로 고정 해놓고 사용합니다. 

@Composable
fun TestScreen() {
    // onStart() 일때만 호출
 	LifecycleEventEffect(Lifecycle.Event.ON_START) {
        Log.d(tag, "ON_START observed at " + System.currentTimeMillis())
    }
}

 

LifecycleStartEffect

onStart() 가 호출될때마다 사용됩니다. 작업 구문 내부에 onStopOrDispose() 로 onStop() 이 호출할 때의 수행할 작업을 등록 해놓을 수 있습니다. 

@Composable
fun TestScreen() {
    val context = LocalContext.current

    // onStart() 일때 수행
    // key1 은 필수. 변화가 없으면 Unit 써도 됨
    LifecycleStartEffect(key1 = Unit) {            
        val receiver = object : BroadcastReceiver() {
            override fun onReceive(c: Context, i: Intent) {
                //...
            }
        }
        val filter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
        context.registerReceiver(receiver, filter)

		//onStop 일때 아래 작업 수행
        onStopOrDispose {                          
            context.unregisterReceiver(receiver)
        }
    }

    /* UI 내용 … */
}

 

LifecycleResumeEffect
onResume() 가 호출될때마다 사용됩니다. 작업 구문 내부에 onPauseOrDispose() 로 onPause() 가 호출할 때의 수행할 작업을 등록 해놓을 수 있습니다.

@Composable
fun VideoPlayer(
    viewModel: PlayerViewModel = viewModel()
) {
    val context = LocalContext.current
    
    // ExoPlayer 인스턴스
    val player = remember(context) {             
        ExoPlayer.Builder(context).build()
    }

	//onResume() 일때 실행
    LifecycleResumeEffect(key1 = player) {       
        player.play()
		
         // onPasue() 일때 아래 작업 수행
        onPauseOrDispose {                      
            player.pause()
        }
    }

    AndroidView(factory = { PlayerView(context).apply { this.player = player } })
}

 

 

DisposableEffect(key)
인자로 받는 key 의 상태 변화를 기반으로 작업을 수행합니다.  구문 내의 onDispose 로 인해 컴포저블이 해제 될때 수행할 작업도 같이 등록 해놓을 수 있습니다.

@Composable
fun CameraScreen(cameraEnabled: Boolean) {

    // key 의 상태가 바뀔 때마다 DisposableEffect 작업을 수행합니다.
    // onDispose 는 해당 컴포저블이 사라질 때 작업이 수행됩니다.
    DisposableEffect(cameraEnabled) {
        if (cameraEnabled) {
            camera.open()          
        }
        
        onDispose {                  
            camera.close()
        }
    }
}

...
//Unit 으로 키를 주면 “컴포저블이 사라질 때 한 번만 해제” 패턴으로 쓸 수 있습니다.
DisposableEffect(Unit) {
    onDispose {
        // 정리(Dispose) – 등록 해제·리소스 닫기
    }
}

 

 

Compose 의 네비게이션 (화면 이동, 백스택 등)

Compose 는 Single Activity 을 지향하고 UI는 Compose 로 구성하는 것을 구글에서 권장합니다. 그래서 액티비티 간의 화면 이동에 따른 LifeCycle 을 계산해야했던 것 과 달리 Compose 간의 화면 이동에 주로 신경을 쓰면 됩니다. 그리고 각 Composable 화면간의 생명주기는 NavHost가 관리 합니다.

@Composable
fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    route: String? = null,                 // 이 NavHost 자체를 중첩 그래프로 취급할 때
    builder: NavGraphBuilder.() -> Unit    // 이 블록 안에서 'composable', 'navigation' 호출
)

 

NavHost는  NavController 가 들고 있는 백스택을 관찰하면서 Composable를 실제로 화면에 렌더링 해주는 View Layer 입니다. 즉, 어떤 Route를 보여줄지 결정하고 Coposition 트리를 교체하는 역할을 맡습니다. 

 

아래는 예시 코드입니다. 

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent { AppRoot() }
    }
}

@Composable
fun AppRoot() {
    val navController = rememberNavController()
    val windowSize = calculateWindowSizeClass(LocalContext.current as Activity)
    val stateHolder = rememberSaveableStateHolder()

    MaterialTheme {
        // Bottom-Nav 예시: 화면마다 독립 State 보존
        Scaffold(
            bottomBar = { BottomBar(navController) }
        ) { innerPadding ->
            NavGraph(
                navController = navController,
                stateHolder = stateHolder,
                modifier = Modifier.padding(innerPadding)
            )
        }
    }
}
  • calculateWindowSizeClass : 태블릿/폴더블 대응.
  • rememberSaveableStateHolder : 탭 간 전환에도 각 화면의 스크롤·폼 입력을 따로 보존

NavController 는 화면 전환, 백스택, 딥링크, 상태 복원을 모두 맡는 싱글 인스턴스 객체입니다. NavController 가 현재 어떤 경로로 되어있는지에 따라 해당 화면을 렌더링 하고 NavController 를 이용해서 화면을 전환하기도 합니다. 위 코드는 구성 변경에도 같은 인스턴스가 유지될 수 있도록 rememberNavController() 를 통해 navController를 생성해서 navController를 NavGraph로 넘깁니다.

그외 작업은 바텀 네비게이션 바를 세팅하거나 ui 규칙 Base를 만듭니다. 

 

sealed interface Screen {
    val route: String
    data object Home : Screen { override val route = "home" }
    data object Detail : Screen { override val route = "detail/{id}" }
    // ...
}

@Composable
fun NavGraph(
    navController: NavHostController,
    stateHolder: SaveableStateHolder,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Screen.Home.route,
        modifier = modifier
    ) {
        composable(Screen.Home.route) {
            // ─── key = route ───┐
            stateHolder.SaveableStateProvider(Screen.Home.route) {
                HomeScreen(
                    onOpenDetail = { id -> navController.navigate("detail/$id") }
                )
            }
        }
        composable(
            route = Screen.Detail.route,
            arguments = listOf(navArgument("id") { type = NavType.LongType })
        ) { backStackEntry ->
            val id = backStackEntry.arguments!!.getLong("id")
            DetailScreen(id = id)
        }
    }
}

 

NavHost 내에서 어떤 composable 의 렌더링을 세팅할지 정하고 navController 를 이용해 화면 전환 리스너 세팅을 해줍니다. 그리고 stateHolder 로 감싸 HomeScreen 을 호출하고 있는데 이는 탭 간 전환에도 화면의 스크롤 및 폼 입력을 보존하기 위함입니다.  

 

전체 ui 구조 예시

MainActivity
   └─ setContent { AppRoot() }
          ├─ rememberNavController()
          └─ Scaffold
               ├─ BottomBar(navController)    ← ③
               └─ NavHost(navController)
                    ├─ HomeScreen            ← route "home"
                    └─ DetailScreen          ← route "detail/{id}"

 

이렇게 네비게이션으로 Composable 간의 화면 전환을 관리하면서 액티비티 간 Lifecycle 에 대한 고민이 많이 사라졌고 컴포저블의 생명주기는 컴포지션 트리 진입 및 탈퇴에 따른 구조라서 매우 단순하기 때문에 ui 개발하기가 편리해졌습니다.

 

Compose 의 라이프 사이클 

1. 탄생 (Composition)

  • Composable 함수가 처음 호출되는 부분 입니다.
  • remember 로 초기값이 만들어지고, 코루틴 및 리스너를 담을 Effect훅이 준비됩니다.
  • 이때 Recomposer가 어떤 State를 읽었는지 의존 관계를 기록해둡니다.
  • 마지막 순서로 ui가 그려집니다. 

2. 변화 (Recomposition)

  • State 값이 바뀌면 Recomposer가 먼저 "변한 State -> 영향을 받는 Composable" 관계를 찾아냅니다.
  • 해당 Composable만 다시 실행해 레이아웃/드로우를 갱신하고 나머지를 건드리지 않습니다.
  • remember 의 메모리 재할당 없이 블록이 실행되지 않으며 UI는 불필요한 재갱신을 하지 않아 오버헤드가 최소화됩니다.

3. 퇴장 (Disposal) : 뒤로가기·탭 전환처럼 화면에서 사라질 때 정리한다.

  • 노드가 트리에서 완전히 빠지는 순간 발생합니다.
  • 뒤로가기, 탭 전환 등으로 인해 발생합니다.
  • 외부 컴포저블의 recomposition 에 의한 상태 변화시 조건문 분기에서 제외 될때 Disposal 이 발생할 수 있습니다.
@Composable
fun Counter() { 
//...
}

fun Test() {
	var show by remember { mutableStateOf(true) }

    //show 가 true 였다가 false 로 바뀌면 Disposal 상태 
	if (show) Counter()   
}

 

컴포저블의 생명주기를 알아봤습니다. 탄생, 변화, 퇴장으로 간단한 3개의 흐름을 가지고 있으며 [탄생 -> 퇴장] or [탄생 -> 여러번 변화 -> 퇴장] 등 초기 컴포지션 이후에 상태값에 따라 생명주기가 흘러갑니다. 그만큼 기존 View Class 와 달리 상태값 관리에 신경을 많이 써야 할겁니다.  


Compose 의 구성 변경을 대비한 Bundle 저장

언제 써야 하나?  대표 예시  권장 API
한-화면(UI) 전용·일시적 상태 (스크롤 위치, 입력 중 텍스트 등) LazyListState·TextField 내용 rememberSaveable / rememberSaveableStateHolder
화면 간 공유하거나 장수 해야 하는 상태 편집 중 폼, 페이지네이션 결과 ViewModel (+ SavedStateHandle)

 

1. UI 전용 상태 rememberSaveable 를 사용하면 SavedInstanceState 에  저장하기 때문에 rememberSaveable 은 프로세스가 완전히 죽은 뒤 재시작할 때도 복원됩니다.

var query by rememberSaveable { mutableStateOf("") }

 

2. 다중 화면(Navigation) 상황에서 rememberSaveableStateHolder 를 사용하면 route 키 별로 Bundle 을 관리해줘서 탭을 왕복해도 각 탭의 스크롤 위치 및 입력값이 유지됩니다.

val holder = rememberSaveableStateHolder()
holder.SaveableStateProvider(route) {
    ScreenContent()   // 내부에서 rememberSaveable 사용 가능
}

 

3. viewModel에 번들 저장하기는 ViewModel + SavedStateHandle 을 통해 가능합니다. ViewModel은 재구성과 무관 하므로 프로세스가 살아있는 한 유지 되어야 할 상태에 적합합니다. 

class DetailViewModel(state: SavedStateHandle) : ViewModel() {
    var uiState by mutableStateOf(state["ui"] ?: UiState())
    override fun onCleared() { state["ui"] = uiState }
}