" Park 기술 블로그 "
# 카테고리
# 도구
[Android 안드로이드] Activity에 대해서
2020-12-24 15:17:08

 안녕하세요!

오늘은 안드로이드 4대 컴포넌트 중 하나인 Activity에 대해서 소개해드리겠습니다. 어쩌면 가장 쉽게 접할 수 있어 친숙하지만 가장 많은 내용을 갖고있는 컴포넌트인 것 같습니다.

 

먼저 Activity란,

" Activity 클래스는 Android 앱의 중요한 구성요소로 Activity가 실행되고 결합되는 방식은 플랫폼 애플리케이션 모델의 기본 요소입니다. main() 메서드를 사용하여 앱을 실행하는 프로그래밍 패러다임과 달리 Android 시스템은 수명 주기의 특정 단계에 해당하는 특정 콜백 메서드를 호출하여 Activity 인스턴스의 코드를 시작합니다.

...

 Activity는 앱이 UI를 그리는 창을 제공합니다. 이 창은 일반적으로 화면을 채우지만 화면보다 작고 다른 창 위에 떠 있을 수 있습니다. 일반적으로 한 Activity는 앱에서 하나의 화면을 구현합니다. 예를 들어 앱의 Activity 중 하나는 환경설정 화면을 구현하고 또 다른 Activity는 사진선택 화면을 구현할 수 있습니다.

 모바일 앱 환경은 사용자와 앱의 상호작용이 항상 동일한 위치에서 시작되는 것이 아니라는 점에서 데스크톱 앱 환경과 다릅니다. 대신 사용자 여정은 흔히 비결정론적으로 시작됩니다. 예를 들어 홈 화면에서 이메일 앱을 열면 이메일 목록이 표시될 수 있습니다. 이에 반대로 소셜 미디어 앱을 사용하고 있는 상태에서 이메일 앱을 실행하면 이메일을 작성하기 위한 이메일 앱 화면으로 바로 이동할 수 있습니다.

...

 앱의 Activity를 사용하려면 앱의 manifest에 Activity 관련 정보를 등록하고 Activity 수명 주기를 적절히 관리해야 합니다. "

라고 안드로이드 공식 문서에서 소개하고 있습니다.

 

 간단하게 설명하자면 Activity는 한 화면을 사용자에게 제공하는 UI 컴포넌트입니다. 하지만 모든 경우에 휴대폰 화면을 가득 채우는건 아니며 때에 따라 화면보다 작을 수도 있고 권한 요청 API처럼 Dialog 형태로 다른 Activity 위에 떠있을 수 있습니다.

 기존의 많은 언어들에는 Entry Point 함수가 있습니다. 흔히 Entry Point는 main이라 불리는 함수인데 Entry Point라는 어원과 동일하게 프로그램의 시작지점을 담당합니다. 하지만 여러 앱을 사용하다보면 앱 런처 아이콘을 통해서 앱에 접근할 때도 있지만 타 앱을 통해 접근하거나 상단의 알람을 통해 앱의 시작 화면이 아닌 중간 화면에 접근하여 이용할 수도 있습니다. 그렇기 때문에 모바일에서는 앱의 시작점에서 시작한다는 보장을 할 수 없습니다. 이러한 패러다임을 대응할 수 있도록 설계된 것이 Activity 컴포넌트입니다.

 


 

 그렇다면 어떤 방식으로 대응할 수 있는지에 대해 소개해드리겠습니다.

Activity는 각각의 생명 주기가 있습니다. 이 생명 주기에 맞춰서 Callback 메서드를 호출하는데 각 생명 주기에 맞는 Callback 메서드에 기능을 구현하면 됩니다.

아래는 Activity의 생명 주기에 대해 한 눈에 볼 수 있도록 순서도로 표기한 사진입니다.

 

(출처: https://developer.android.com/guide/components/activities/activity-lifecycle?hl=ko#alc)

 위의 Callback 메서드 중에서 필수적으로 구현해야하는 메서드는 onCreate() 하나이며 이 메서드 안에는 setContentView 메서드 호출을 통해 실질적으로 사용자에게 보여지는 화면을 정의한 layout xml과 연결합니다. 데이터바인딩을 구현하기 위해서는 이 메서드를 내부적으로 호출하는 다른 메서드를 사용할 수도 있지만 이번 포스팅에서는 기본적인 사용법과 구성에 대해 소개해드리겠습니다 : )

 

onCreate()

 이 메서드는 안드로이드 시스템이 Activity를 생성할 때 실행되는 메서드입니다. 보통 Activity가 생명 주기 중 한 번만 실행되야하는 소스들이 주로 담기며 주로 멤버 변수 초기화나 UI에 대한 기본 설정을 합니다. 그리고 해당 메서드 안에서 setContentView 메서드가 호출되어야 합니다. 호출되지 않아도 에러가 발생하진 않지만 layout xml에 정의한 화면이 표시되지 않으며 다른 생명 주기의 Callback 메서드에서 호출해도 표시되지 않습니다. 뿐만 아니라 상속받는 부모클래스의 onCreate()메서드보다 먼저 호출해도 작동하지 않습니다. 그리고 setContentView 메서드를 정상적으로 호출하지 않을 경우 findViewById() 메서드를 통해 View에 접근했을 경우에도 null이 리턴됩니다. 그리고 이 메서드 안에서 finish 메서드를 통해 Activity를 종료시킨다면 onStart-onResume-onPause-onStop 메서드가 생략되고 바로 onDestroy 메서드가 호출됩니다.

 이 메서드는 매개변수로 Bundle 인스턴스인 savedInstanceState를 전달 받습니다. 휴대폰이 회전했을 경우 혹은 메모리의 부족으로 정지된 Activity 중에서 재생성 됐을 때 등 이전 Activity의 저장 상태가 담긴 인스턴스입니다.

 

onStart()

 이 메서드는 Activity가 사용자에게 표시됩니다. 하지만 layout xml에 정의한 화면은 아직 나오지 않는 단계입니다. 주로 앱이 UI를 관리하는 코드를 초기화하는

메서드 입니다. 혹여 해당 Activity가 onStop 상태에서 다시 포그라운드로 오게된다면 onRestart 메서드 호출 후 이 onStart 메서드가 호출됩니다. 또한 onStart에서 finish 메서드를 통해 Activity를 종료한다면 바로 onStop이 호출되기 때문에 onResume과 onPause 메서드는 호출되지 않습니다.

 공식 문서에서는 이 onStart 메서드가 시각적인 요소를 표시하고, 애니메이션을 실행하기 적합한 메서드라고 설명하고 있지만 실제로 제가 많이 사용하지 않았던 메서드라 친숙하지 않았습니다. 여러 문서를 찾아보니 onStart 메서드에서는 통신, 센서 제어 같은 작업이 주로 이루어진다고 합니다.

 Android 7.0 Nougat 버전부터 지원하기 시작한 화면 분할 모드, 즉 멀티윈도우 기능이 추가되면서 onStart의 메서드가 더 중요하게 바뀐 것 같다는 생각을 했습니다.

멀티윈도우 상황에서 Activity에 포커스를 잃게되면 onPause를 호출하고 포커스를 얻게되면 onResume이 호출되기 때문입니다.

 예를 들어 멀티윈도우에서 오른쪽에는 넷플릭스같은 OTT서비스를 켜놓고 왼쪽에서 웹서핑을 하고 있는 상황이라고 했을 때, 왼쪽 웹서핑을 하는동안 오른쪽의 영상 재생되어야 합니다. 만약 onStart - onStop - onRestart 메서드에서 영상 시작, 정지를 제어하지 않고 onResume - onPause에서 제어했다면, 오른쪽의 OTT서비스 앱을 이용하다가 왼쪽의 웹서핑 브라우저 앱을 터치하여 포커스가 넘어갔을 때 OTT서비스 앱에서는 onPause가 호출되어 재생이 일시 정지될 것입니다. 이후 다시 OTT서비스 앱을 터치하여 포커스가 돌아오게되면 onResume 메서드가 호출되어 영상이 재생될 것입니다.

 이러한 상황을 토대로 생각했을 때, 화면의 포커스를 고려하여 포커스를 잃은 경우에도 작동이 되어야하는 작업은 onResume 대신 onStart를 사용해야할 것 같습니다. 그리고 OTT서비스 앱에서 onPause 호출 후 onStop이 호출되도록 홈화면으로 나가거나 다른 타 앱을 켜 화면을 전부 가린 경우, OTT서비스 앱으로 다시 돌아왔을 때 일시정지가 되어있어야 합니다. 이런 경우를 생각해서라도 onStart, onStop, onRestart 메서드를 활용해서 구현하면 좋을 것 같습니다.

 

onResume()

 이 메서드는 사용자와 상호작용을 시작하기 직전에 호출되며 Activity 스택의 최상위에 쌓이게 됩니다. 이 메서드가 호출되고 나서 사용자와 상호작용을 시작하는데 휴대폰의 화면이 꺼지거나, 알람이 울리거나, 전화가 오는 등의 다른 방해 이벤트가 발생하여 Activity에서 포커스가 떠날 때까지 앱은 재개됨 상태에 머무르게 됩니다.

그리고 onPause에서 해제되는 소스들을 이 메서드에서 초기화 해주어야하며, onStart에서 설명했던 것처럼 포커스를 얻었을 때를 초기화 해야하는 부분들을 적용하면 됩니다. 공식 문서에서는 앱의 핵심 기능 대부분이 onResume 메서드에서 구현된다고 소개하고있습니다.

 

 여기서 중요한 것은 onCreate, onStart, onResume의 메서드들은 호출되고나서 대기하는 것이 아니라 바로바로 이어서 호출됩니다. onCreate부터 호출됬으면  onStart, onResume이 순서대로 이어서 호출되고, onStart부터 호출됬으면 onResume 메서드가 이어서 호출됩니다.

 

onPause()

 이 메서드는 Activity가 포커스를 잃거나 일부분 가려졌을 경우 호출되며 포그라운드에 있지 않게됩니다. 그러나 onStart 메서드를 소개할 때 설명했던 것과 같이 멀티윈도우의 상황에서 일부분이 가려지지 않았지만 포커스만 잃는 경우에도 onPause가 호출됩니다. 주로 휴대폰 화면이 꺼지거나, 알람 울리는 경우, 전화오는 경우 등과 같은 것이 가장 일반적인 사례입니다. 뿐만 아니라 Dialog로 인해 부분적으로 가려졌을 경우에도 onPause가 호출됩니다. 그러나 Dialog 클래스를 상속받아서 구현한 커스텀 다이어로그나, AlertDialog 등의 Dialog는 Activity의 일부분으로 보기 때문에 onPause가 호출되지 않습니다.

ActivityCompat.requestPermissions 메서드로 호출되는 Dialog는 다른 Activity가 생성되어 기존의 Activity의 일부분을 가리기 때문에 onPause가 호출됩니다.

안드로이드 공식 문서에서는 이 onPause 메서드가 아주 잠깐 실행되므로 저장 작업을 실행하기에는 시작이 부족할 수 있으니 데이터 저장, 네트워크 호출, DB 트렌젝션을 실행해서는 절대 안된다고 강조하고 있으며 이와 같은 작업은 onStop 메서드에서 구현하도록 권장하고 있습니다.

 이 메서드가 호출되고 Activity가 일시중지됨 상태에 들어가면 메모리 부족 시 안드로이드 시스템에 의해서 재생성될 수 있으며 재생성될 때는 인스턴스 재할당이 이루어지며 onCreate 메서드부터 다시 시작됩니다.

 

onStop()

 이 메서드는 사용자에게 Activity가 더 이상 표시되지 않을 때 호출됩니다. 예를 들어서 새로운 Activity가 호출되어 기존의 Activity를 덮어서 보이지 않게 되었을 경우, 홈버튼을 통해 홈화면으로 갔을 경우 등 입니다. 이 메서드에서는 사용자에게 화면이 보이지 않을 때 필요 없는 리소스를 해제하거나 수정해야 합니다.

애니메이션 기능을 중지 하거나 위치 추적에 대한 업데이트 시간을 늘리는 등의 조치를 취합니다. 이 메서드가 호출되고 Activity가 중단됨 상태에 들어가면 메모리 부족 시 안드로이드 시스템에 의해서 재생성 될 수 있으며 재생성될 때는 인스턴스 재할당이 이루어지며 onCreate 메서드부터 다시 시작됩니다. 이 onStop 메서드가 호출되고 중지됨 상태에서 다시 Activity가 포그라운드로 올라왔을 경우에는 onRestart 메서드가 호출된 뒤 onStart, onResume이 호출됩니다.

 

onDestroy()

 이 메서드는 Activity가 소멸되기 전에 호출됩니다. 사용자가 뒤로가기 버튼을 통해서 Activity를 종료할 때, 앱 내부적으로 finish 메서드를 호출해서 종료되는 경우, 휴대폰의 방향이 회전되거나 멀티윈도우모드로 진입할 때 시스템에서 Activity를 소멸시키는 경우 등 호출됩니다. 그리고 사용자가 뒤로가기 버튼을 통해 Activity를 종료하거나 finish 메서드를 호출해서 종료하는 경우 isFinishing 메서드를 통해서 구분할 수 있습니다. 시스템에서 onDestroy 메서드를 호출하게되면 새로운 인스턴스를 재할당하고 onCreate메서드를 이어서 호출합니다.

리소스 제약으로 인해 프로세스가 종료되면 생명 주기에 해당 하는 다음 Callback 메서드들이 호출되지 않고 종료된다고 합니다. 앱 프로세스를 중단했을 경우 추가적인 Callback 메서드가 호출되지 않고 종료되는 것 같습니다.

(참고 https://medium.com/androiddevelopers/viewmodels-persistence-onsaveinstancestate-restoring-ui-state-and-loaders-fc7cc4a6c090)

 

 여기서 또 중요한 부분이 있습니다!

Activity 생명 주기 순서도 그림을 참고하시면 알 수 있지만 onCreate, onStart 안에서 finish 메서드를 호출하여 일부 메서드가 생략되는 특별한 경우 등을 제외하고, onStop이 호출되려면 onPause가 선행되어야하고 onDestroy 메서드가 호출되려면 onPause와 onStop이 선행되어야 합니다. 그렇기에 현재 Activity가 전부가려지면 onStop만 호출되는 것이 아니라 onPause메서드도 같이 호출됩니다.

 그러면 onPause만 호출되는 시기가 있을까하고 알아보았습니다. onPause의 설명에서와 같이 ActivityCompat.requestPermissions 메서드를 통해 권한 요청 API를 호출 하는 경우나, 멀티윈도우에서 포커스가 옮겨졌을 경우 등이 있었습니다.

 그러나 앱 이용 중 전화가 왔을 때, 알람이 울렸을 때 등은 시뮬레이터로 테스트 해봤을 때 버전별로 상이한 것을 확인할 수 있었습니다. 최근 버전의 휴대폰들은 앱 이용 중에 전화나 알람이 울렸을 때 화면 상단에 팝업창이 뜨면서 알려줍니다. 이럴 때는 onPause 메서드도 호출되지 않았습니다. 그러나 Kitkat 버전 시뮬레이터로 테스트 했을 때는 전화나 알람이 울렸을 때 팝업창이 뜨는 것이 아니라 화면이 전환되어서 onStop 메서드까지 호출됨을 확인할 수 있었습니다.

(이 포스팅을 하다가 알게되었는데 재난문자가 수신되면 보여지는 SMS팝업창도 onPause만 호출됬습니다. - API 28 Pie)

 


 

 위의 내용과 같이 Activity의 각 생명 주기별 특성을 파악하고 적절한 시기에 기능을 재정의하게 된다면 안정적인 앱을 개발할 수 있습니다.

이번에는 Activity가 재생성 됐을 때 값을 저장하고 복원하는 방법에 대해서 알아보려 합니다. 화면이 회전했을 때나 메모리 부족으로 인해, 멀티윈도우로 전환했을 때 등으로 Activity가 재생성 됐는데 내용이 없어지게 된다면 치명적인 오류일 것입니다. 이에 대한 적절한 조치 사항으로 여러 방법이 있지만 안드로이드 공식 문서에서 가이드된 3가지 방법에 대해서 살펴보려고 합니다. 

 

 

 ViewModel은 MVVM 아키텍처에 대해서 포스팅할 때 자세하게 다룰 예정입니다. 지금은 View와 Model 사이에서 연결해주는 역할을 담당하며 Model에서 가져온 데이터를 View에 적용할 수 있도록 가공과 제공하며 저장하는 역할입니다. 위의 표와 같이 저장 위치가 메모리기 때문에 프로세스 중단 시에는 소멸되는 휘발성 저장소이며 저장공간이 보조메모리 대비 제한적입니다. 하지만 반대로 저장 위치가 메모리이기 때문에 데이터에 접근하는 속도가 빠르고 이미 메모리에 올라온 복잡한 객체에도 쉽게 접근할 수 있습니다.

 저장된 인스턴스 상태는 savedInstanceState가 번역된 것입니다. 이 방법은 onSavedInstanceState와 onRestoreInstanceState 메서드를 재정의해서 사용할 수 있습니다. 이 두 메서드를 재정의하지 않아도 적용되는 기본 동작은 EditText 안의 문자열과 ListView의 스크롤 위치 정보 등, 간략한 UI 정보들을 자동으로 저장하고 복원하는 기능입니다.

 이 메서드를 통해 UI를 복원하고자 한다면 먼저 onStop메서드 다음에 항상 호출되는 onSavedInstanceState를 통해 값을 저장해야 합니다. 매개변수로 전달받는 Bundle 인스턴스인 outState에 데이터를 put하면 저장할 수 있습니다. 그러나 공식 문서에서는 이 onSavedInstaceState가 간단한 데이터를 저장하기에 적합하도록 설계되었다고 설명하고 있습니다. 그렇기에 int, double, boolean 등과 같은 원시 자료형과 간단한 String 인스턴스를 저장할 수 있으며 Parcelable을 구현한 Reflection이 필요없는 인스턴스 등을 저장할 수 있습니다. 그리고 Serializable을 통해 Reflection이 필요한 직렬화된 인스턴스도 저장할 수 있는데 대량의 데이터 혹은 길이가 긴 직렬화나 역직렬화가 필요한 복잡한 데이터 구조를 저장하는 것은 지양하도록 가이드하고 있습니다.

이렇게 onSavedInstanceState를 통해 데이터를 저장했다면 onStart 다음에 호출되는 onRestoreInstanceState를 통해 값을 가져와 UI에 복원해야 합니다. 그런데 이 onRestoreInstaceState 메서드는 onSavedInstanceState와 다르게 항상 호출되는 것이 아닙니다. 화면의 회전 혹은 메모리 부족으로 인한 재생성, 멀티윈도우로 진입했을 때의 재생성 등 안드로이드 시스템에서 Activity를 재성성하는 경우에만 onRestoreInstanceState를 호출합니다. 예를 들어 다른 Activity로 갔다가 다시 돌아 왔을 때 만약 메모리가 부족하지 않아 시스템에 의해 재생성되지 않았다면 이전 UI 정보 그대로 남아있을 것입니다. 하지만 돌아왔을 때 메모리가 부족해서 이미 시스템에 의해 재생성 됐다면 onRestoreInstanceState에 정의된 복구 로직을 따라 이전 UI 상태로 복구되어 사용자에게는 정상적으로 돌아온 것으로 보여질 것 입니다.

  영구 저장소는 SharedPreferences, SQLite, 원격저장소 등과 같은 저장소입니다. SharedPreferences와 SQLite는 휴대폰 기기 안에 저장되는 것이고 원격저장소는 서버에 저장되는 저장소입니다. 쉽게 말해서 휴대폰 기기를 재부팅 했을 때 데이터가 유지될 수 있는 저장소를 의미합니다. 이 savedInstanceState와 영구 저장소처럼 디스크에 저장되는 것들은 메모리에서 직접 접근하는 것보다 상대적으로 시간이 필요하기 때문에 각자의 장단점을 확실히 알고 적절한 상황에 활용해야 합니다.

 


 

 이제 Activity를 실행하고 전송하는 법에 대해서 알아보겠습니다.

먼저 Activity를 실행하려면 Intent 인스턴스를 활용해야 합니다. 이 Intent는 Activity 뿐만 아니라 다른 컴포넌트들에게 작업을 전달하는 역할을 합니다. 예를 들어 Activity에서 startService 메서드를 통해 Intent를 전달하여 Service를 실행시킬 수 있고 sendBroadcast 메서드를 통해 Intent를 전달하여 Broadcast를 송신할 수도 있습니다. 이렇듯 startActivity를 통해 Intent를 전달함으로써 다른 Activity를 실행할 수 있습니다. 뿐만 아니라 Intent 인스턴스에는 Bundle 인스턴스를 담아서 전송할 수도 있고 원시자료형 및 String 인스턴스, Parcelable을 구현한 Reflection이 필요없는 인스턴스 등을 담아서 전송할 수 있습니다.

 

 Activity를 실행하는 방법은 두가지가 있습니다. 하나는 startActivity 메서드를 통해서 실행만 하는 것과 startActivityForResult 메서드를 통해서 실행하는 것입니다. 이 두가지의 차이점은 startActivity를 통해 실행하면 실행한 Activity가 종료되어 돌아올 때 종료된 Activity에서 결과값을 받아올 수 없고 startActivityForResult메서드를 통해 호출하면 새로 실행한 Activity로부터 결과값을 가져올 수 있습니다. 이 경우에는 결과값을 리턴하는 Activity에서 setResult 메서드를 통해 작업 결과 코드만 결과값으로 전달할 수도 있고 두 번째 아규먼트로 데이터를 담은 Intent 인스턴스를 전달할 수도 있습니다. 이렇게 setResult 메서드로 결과값을 전달했다면 결과값을 받을 Activity에서 onActivityResult 메서드를 재정의해서 파라미터로 전달받은 결과 코드와 데이터가 담긴 Intent를 통해 기능을 구현합니다.

 


 

 위와 같은 방법으로 다른 Activity를 호출했다면 어떤 순서로 Activity가 관리될까요?

그 단위는 바로 Task라는 단위로 Stack 자료구조를 통해 관리됩니다. 먼저 들어오는 것이 나중에 나가는 FILO(First In Last Out) 방식으로 관리됩니다. 그렇다면 Task는 무엇인지 궁금할 것 같습니다. 흔히 휴대폰의 사용자 입장에서 앱의 Activity 관리 단위라고 생각하면 좋을 것 같습니다. 하지만 하나의 앱에서 여러개의 Task를 갖을 수도 있습니다. 예를 들어 메일 앱에서 여러 Activity를 생성하며 작업을 한 뒤 홈버튼을 통해서 홈화면으로 이동 후, 갤러리 앱을 이용했다고 가정합니다. 이럴 경우 메일 앱에서 여러 Activity를 생성하며 작업했던 것이 하나의 Task로 관리되며 홈버튼을 통해서 앱이 백그라운드로 전환되면 메일 앱의 Task가 백그라운드로 관리됩니다. 이후 갤러리 앱을 이용하면서 갤러리 앱 Task를 통해 갤러리 앱의 Activity가 관리됩니다. 그 다음에 다시 메일 앱을 터치한다면 메일 앱의 Task가 백그라운드로 관리되다가 포그라운드로 전환되서 이전의 Activity들을 다시 이용할 수 있는 것입니다. 이것이 Task의 개념과 멀티태스킹의 작동방식입니다.

 

 그렇다면 하나의 Task, 즉 한개의 앱에서 여러 Activity가 실행되고 관리될 때를 설명해보려 합니다. 예를 들어 A, B, C Activity가 정의되어 있고 현재 A -> B -> C 순서로 Activity를 실행한 상황에서 C Activity를 한번 더 실행하면 Task에서는 A -> B -> C -> C와 같이 C Activity 인스턴스를 한개 더 생성해서 Task에 관리하게 됩니다. 이것이 기본 동작입니다. 하지만 Activity의 launchMode 속성을 수정해서 Task에서 제일 위에 있는 Activity에 한에서만 동일한 인스턴스를 생성하지 못하게 할 수도 있습니다. 이런 속성을 적용하는 법에는 AndroidManifest.xml의 activity 태그 안에 launchMode 속성에 설정하는 법과 Intent를 생성한 후 startActivity 메서드를 호출할 때 Intent 인스턴스에 전에 추가된 Flag를 지우고 Flag를 셋팅하는 setFlag 메서드, 혹은 기존의 Flag에 대해 추가하는 addFlag 메서드를 통해 적용하는 법이 있습니다. addFlag 메서드는 기존에 추가된 Flag가 없어도 사용할 수 있습니다 : )

 AndroidManifest에 사용할 수 있는 launchMode 동작 중에 Intent의 Flag로 대신 사용할 수 없는 것도 있으며 Intent의 Flag로 사용할 수 있는 동작들 중에 AndroidManifest에서 대신 사용할 수 없는 것도 있습니다. 그리고 AndroidManifest에 launchMode가 설정되어 있지만 Intent에서 Flag를 설정한다면 Intent의 Flag 설정을 AndroidManifest의 launchMode보다 우선 시 합니다.

 

 먼저 AndroidManifest.xml에 설정할 수 있는 launchMode 중요 속성은 아래와 같습니다.

  • standard : 위에서 설명한 기본 동작처럼 한개의 Task에서 여러개의 Activity 인스턴스가 존재할 수 있고 현재 Task 외에 다른 Task에서도 Activity 인스턴스가 존재할 수 있습니다. 
  • singleTop : 위에서 기본 동작 다음에 설명한 것 처럼 Task에서 제일 위에 있는 Activity에 한에서만 동일한 인스턴스를 생성하지 못하게 할 수 있습니다. 만약 Task의 최상위에 있는 Activity 인스턴스와 동일한 Activity를 실행하게 된다면 기존의 최상위 Activity 인스턴스에 onNewIntent 메서드를 호출합니다.
  • singleTask : singleTask로 설정된 Activity를 실행했을 때 affinity에 맞는 Task가 없을 경우 Task를 생성한 다음 Activity 인스턴스를 생성해서 Task에 넣습니다. 하지만 Activity 인스턴스는 없지만 이미 affinity에 맞는 Task가 존재할 경우 기존의 Task에 Activity 인스턴스를 생성해서 넣습니다. 단, 이미 Activity 인스턴스가 존재한다면 새로 생성하지 않고 기존의 인스턴스의 onNewIntent 메서드를 호출합니다.
  • singleInstance : singleInstance로 설정된 Activity를 실행하면 동일한 affinity가 존재하더라도 새로운 Task를 생성한 다음 Activity 인스턴스를 생성해서 Task에 넣습니다. 그러나 이미 Activity 인스턴스가 존재할 경우 기존의 있는 Activity 인스턴스의 onNewIntent 메서드가 호출됩니다. 얼핏 보면 singleTask와 동일해보이지만 singleInstance는 하나의 Task에 Activity 인스턴스 한개만 존재할 수 있다는 것입니다. 만약 같은 Affinity를 갖는 다른 Activity 인스턴스가 실행되는 경우에는 같은 Affinity를 갖지만 다른 Task를 생성해서 넣습니다. 이후 해당 Affinity에 대응되는 Task에 추가되는 Activity가 있으면 방금 새로 생성한 Task에 쌓입니다. 쉽게 얘기하면 singleInstance로 인해 생성된 Task는 한개의 Activity 인스턴스만 갖을 수 있으니까 냅두고 같은 Affinity를 갖으며 여러개의 Activity 인스턴스를 허용할 수 있는 Task를 만들어 이 Task에 추가하는 것입니다.


 위와 같이 4개의 launchMode 속성이 있습니다. 사실 standard와 singleTop은 쉽게 이해할 수 있으나 singleTask와 singleInstance는 이해하기 힘들었습니다.

먼저 나머지 Intent에 추가할 수 있는 주요 Flag를 이어서 소개하고 마지막에 제가 이해할 때 도움이 되었던 비교 예제 실습을 마지막에 소개해드리겠습니다 : )

 

 Intent 인스턴스에 추가할 수 있는 3가지의 주요 Flag입니다.

  • FLAG_ACTIVITY_NEW_TASK : 위에서 설명해드린 singleTask와 동일한 작동을 합니다. 해당 Flag가 설정된 Activity의 Affinity에 대응되는 Task가 존재하지 않는다면 해당 Task를 새로 생성하고 Activity 인스턴스를 생성해서 넣습니다. 하지만 Task 이미 존재한다면 Task 안에 Activity 인스턴스를 생성해서 넣고 만약에 Task도 존재하고 Activity 인스턴스도 이미 존재하면 기존 Activity 인스턴스의 onNewIntent 메서드를 호출합니다.
  • FLAG_ACTIVITY_SINGLE_TOP : 위에서 설명해드린 singleTop과 동일한 동작을 합니다. Task의 최상단에 존재하는 Activity 인스턴스가 다시 호출되면 새로운 Activity 인스턴스를 생성하지 않고 기존의 Activity 인스턴스의 onNewIntent 메서드를 호출합니다.
  • FLAG_ACTIVITY_CLEAR_TOP : 실행할 Activity가 이미 Task 안에 인스턴스로 존재하고 쌓여있다면 실행할 Activity 인스턴스 위에 쌓인 다른 Activity 인스턴스들을 제거하고 기존의 Activity 인스턴스의 onNewIntent 메서드를 호출합니다. 해당 Flag와 동일한 작업을 수행하는 launchMode 속성은 없습니다.

 

 이 3가지 외에도 FLAG_ACTIVITY_MUTILFLE_TASK, FLAG_ACTIVITY_CLEAR_TASK, FLAG_ACTIVITY_NO_HISTORY, FLAG_ACTIVITY_NO_ANIMATION 등 여러 Flag가 있습니다 : )

 

 안드로이드 공식 문서에서는 launchMode나 Intent의 Flag를 사용할 경우 사용자가 앱을 이용하면서 뒤로가기 버튼, 최근 사용 기록 버튼을 통해 앱에 접근하는 경우 예상하는 화면과 다른 결과가 초래할 수 있기 때문에 사용하게되면 아래의 사진과 같이 사용성 테스트를 하도록 권장하고 있습니다.

 

 


 

 이제 이론적인 이야기는 대부분 끝이 난 것 같습니다. 위에서 설명한 내용 외에도 많은 내용들이 있지만 다음 기회에 깊게 다루어보도록 하겠습니다.

이번에는 간단하게 실습했던 내용들에 대해서 정리하겠습니다! 이 게시글 처음부분에 설명해드렸던 생명주기와 상태를 저장, 복원하는 Callback 메서드에 대해서 상황별로 어떤 메소드가 호출되는지와 Task에 Activity가 어떻게 쌓이는지 정리해보겠습니다.

 

 먼저 아래와 같이 Callback 메서드를 구현했습니다.

위는 A Activity이고 아래는 B Activity 입니다. A Activity의 onDestroy 메서드가 짤렸지만 다른 메서드처럼 로그를 남기도록 했습니다.

 

 아래의 4가지의 시나리오에서 어떻게 생명주기와 UI 저장, 복원하는 Callback 메서드가 호출되는지 정리하겠습니다.

1. 앱을 시작해서 A Activity 호출된 후 B Activity를 호출했을 때

 위의 사진과 같이 A Activity가 먼저 순서대로 연속적으로 onCreate - onStart - onResume이 호출되고 A Activity가 런칭됩니다. 그 다음 버튼을 눌렀을 때가 중요한 것 같습니다. A Activity의 onPause가 먼저 호출되고 이후 B Activity의 onCreate - onStart - onResume이 호출됩니다. 그 다음에 A Activity의 onStop과 onSaveInstanceState, onDestroy가 호출됩니다. 위에서도 설명했듯이 onSaveInstanceState는 항상 호출되지만 onRestoreInstanceState 메서드는 안드로이드 시스템이 파괴했을 때만 호출됩니다.

 

2. A Activity에서 B Activity를 호출한 상황에서 뒤로가기 버튼을 눌러 A Activity로 돌아갔을 때

 이 시나리오에서는 이미 B Activity가 호출된 상황입니다. 그 상태에서 뒤로가기 버튼을 누르게 되면 위와 같은 호출이 이루어집니다. 먼저 보여지고 있는 B Activity의 onPause가 호출되고 A Activity의 onRestart - onStart - onResume가 호출되며 마지막으로 B Activity의 onStop과 onDestroy 메서드가 호출됩니다.

여기서 onSaveInstanceState가 호출되지 않는 이유는 사용자가 뒤로가기 버튼이나 앱 내부적으로 finish 메서드를 호출하게되면 앱에서는 해당 Activity를 종료할 것으로 간주하기 때문에 Activity의 상태를 저장하는 onSaveInstanceState 메서드를 호출하지 않습니다.

 

3. A Activity에서 화면을 회전했을 때

 화면을 회전하게되면 안드로이드 시스템에서 기존의 Activity를 파괴하고 재생성하게 됩니다. 물론 시스템에서 재생성 했으니까 UI 상태를 저장, 복원할 수 있는 메서드를 호출해줍니다.

 

4. 테스트 앱 위에 다른 앱이 팝업형태로 Activity를 부분적으로 가리며 포커스가 이동됬을 때와 다시 돌아왔을 때

 이 시나리오는 아래의 사진과 같은 상황에서 팝업으로된 OTT 서비스앱을 터치하여 포커스가 옮겨갔을 때와, 약 2초 후 다시 원래 앱으로 포커스가 돌아왔을 때의 호출 메서드를 정리한 것 입니다. 위에서 설명해드린 것과 같이 Activity의 전체를 가리지 않았으므로 onStop이 호출되지 않았으며 포커스가 옮겨갔기 때문에 onPause가 호출됬습니다.

 

 다음에는 Task에 대해서 실습해보겠습니다. 먼저 아래의 사진과 같이 launchMode가 하나도 설정되어 있지 않아 standard가 적용된 모습입니다.

 A -> B -> C -> D -> D Activity 순으로 이동했을 경우에 Task에 적재되는 순서는 아래와 같습니다.

#뒤의 인덱스 번호순처럼 적재되며 / 표시 다음에 있는 Activity명을 확인할 수 있습니다. 이 후 뒤로가기 버튼을 통해 종료하게 되면 높은 인덱스 번호부터 Pop됩니다. 그 다음 D Activity에 singleTop 속성을 지정한 후 A -> B -> C -> D -> D Activity 순으로 호출하면 아래의 사진들과 같이 적재됩니다.

위와 같이 AndroidManifest에서 launchMode 속성을 singleTop으로 설정하고 테스트하면 아래와 같이 적재됩니다.

최상단의 D Activity 인스턴스가 두 개 생기는 것이 아니라 기존의 인스턴스를 그대로 유지하고 있음을 알 수 있습니다.

 

그 다음에 singleTask입니다.

 위의 사진과 같이 C Activity에 singleTask를 설정하고 실행하면 아래의 사진과 같이 적재됩니다. 하지만 여기서 중요하게 봐야할 부분은 taskAffinitiy입니다.

Affinity는 간단하게 설명하면 Task의 고유아이디입니다. Task를 Affinity로 구분한다고 생각하면 쉬울 것 같습니다. 이 Affinity를 설정하지 않으면 앱 패키지명과 동일하게됩니다. 그렇기 때문에 다른 Affinity를 설정해주어야 새로운 Task를 생성할 수 있습니다. 기존에 앱 패키지명과 동일한 상태에서 launchMode를 singleTask를 설정하게되면 앱 패키지명과 동일한 Task가 이미 존재하기 때문에 새로운 Task를 생성하지 않고 기존에 Task에 추가되기 때문입니다.

 위의 사진과 같이 적재되며 C Activity의 시작을 기점으로 새 Task가 생성된 상태에서 추가로 다른 Activity를 시작하게 되면 새로운 Task에 적재되는 모습을 볼 수 있습니다. 이로인해 알 수 있는 것은, Activity가 새로 적재되는 Task는 무조건 Affinity에 근거하는 것이 아닙니다. 먼저 startActivity를 통해 Activity가 실행되면 Affinity가 달라도 현재 Activity가 속한 Task에 적재되며 FLAG_ACTIVITY_NEW_TASK 혹은 singleTask로 지정한 경우에만 Affinity를 탐색하여 적재합니다. 그렇다면 위의 상황에서 AndroidManifest안에 D Activity에 singleTask를 지정하면 D Activity의 Affinity와 동일한 Task에 적재될까요?

정답은 맞습니다. 위의 사진과 동일하게 Affinity에 맞는 Task를 탐색하여 Affinity와 동일한 Task에 적재됩니다.

 

그 다음에는 singleInstance입니다.

위와 같이 C Activity에 singleInstance로 지정하고 A -> B -> C -> D Activity를 시작하면 아래와 같이 적재됩니다.

 여기서 알 수 있는 것은 singleInstance를 통해 실행되면 Affinity와 동일한 Task가 없다면 새로 생성하고 적재합니다. 그 다음에 새로 호출되는 Activity가 있다면 singleTask나 FLAG_ACTIVITY_NEW_TASK가 지정되지 않더라도 새로 호출되는 Activity의 Affinity를 탐색해서 있으면 적재, 없으면 새로 Task를 생성 후 적재합니다. 왜냐하면 singleInstance는 Task와 Activity가 1대1 매칭하는 것이 원칙이기 때문에 singleInstance로 지정된 Activity가 호출된 후에 새로 호출되는 Activity는 자동으로 Affinity에 맞는 다른 Task를 탐색하는 것입니다.

 singleInstance는 Affinity와 동일한 Task가 있다면 그 Task 안에 해당 Activity에 맞는 인스턴스를 찾고, 있으면 그 해당 Activity 인스턴스의 onNewIntent 메서드를 호출합니다. 그러나 Activity 인스턴스가 없으면 Affinity와 동일한 Task가 있다고 하더라도 singleInstance는 Task와 Activity가 1대1 매칭이 원칙이니 새로 Task를 생성 하고 적재합니다. 이 singleInstance와 singleTask를 통해 Task가 추가되면 최근 사용 기록 버튼을 통해 보면 같은 앱이지만 Task가 생성된 것을 확인할 수 있습니다.

 

 안드로이드 공식문서에서는 이 singleTask와 singleInstance로 인해 생성된 Task들을 사용하려면 여러 사용성 테스트를 해야한다고 합니다. 이 두 속성값을 통해 호출된 Activity들을 사용하다보면 사용성 테스트를 하지 않아 불안정한 앱의 경우 사용자가 뒤로가기 버튼을 통해 이전 화면으로 돌아가려고 할 때 예상하지 못한 다른 화면이 나올 수 있기 때문이며, 이 singleTask나 singleInstance로 실행되어 다른 Task에 적재된 Activity들은 Task가 백그라운드로 돌아갈 경우 다시 포그라운드 로 불러올 수 있는 방법이 거의 없기 때문입니다. 앱 내에서 해당 Activity를 다시 호출하거나, 최근 사용 기록 버튼을 통해 Activity를 다시 불러오는 방법 두가지 외에 는 없기 때문입니다. 앱 런처를 통해 불러오는 Activity는 Main Action과 LAUNCHER Category가 적용된 Intent-filter를 갖는 Activity이며 이 Activity가 속한 Task만 앱 런처를 통해서는 포그라운드로 불러올 수 있기 때문입니다. 포그라운드로 Task를 가져온 후 Task의 최상단에 위치하는 Activity 인스턴스가 화면에 보여집니다.

 

 이상 Activity 컴포넌트에 대해서 소개를 마치겠습니다.

가장 친숙한 컴포넌트이지만 그 만큼 비중이 크고 중요한 컴포넌트여서 내용이 많은 것 같습니다.

궁금하신 내용이나 부족한 부분에 대해서는 댓글로 남겨주시면 감사드리겠습니다 : )

2
# 댓글 + 새 댓글 작성
# 새 댓글 작성
댓글 암호는 댓글 삭제 시 필요합니다.
# 님에게 답변 작성
대댓글 암호는 댓글 삭제 시 필요합니다.
# 님의 댓글 삭제
댓글 작성 시 입력했던 암호를 입력해주세요.
LJY 2021-01-08 14:38:57
좋은 내용 감사합니다.~~~
2021-01-08 18:05:22   ParkSM
감사합니다 : )