[Android Fundamentals] Activity - 6. Tasks and the back stack
Task는 사용자가 뭔가를 하기 위해 상호작용하는 activity의 모음이다. 새로 생성되는 activity는 back stack이라고 불리는 스택에 삽입된다.
예를 들어, 이메일 앱을 켜면 메일 리스트 activity가 보일 수 있다. 사용자가 어떤 메일을 선택하면, 메일의 자세한 정보를 보여주는 activity가 만들어지고, back stack에 추가된다. 사용자가 뒤로 가기 버튼을 터치하면 메일 상세 정보 activity가 종료되고, back stack에서 pop된다.
Task의 생명주기와 back stack
대부분의 task는 기기의 홈 화면에서 시작된다. 사용자가 앱 아이콘을 터치하면 그 앱의 task가 포그라운드에 배치된다. 앱의 task가 없었다면, 새 task가 만들어지고 앱의 main activity가 root activity(back stack에 처음 삽입된 activity)로서 stack에 삽입된다.
다른 activity를 실행하면, 새로 실행된 activity가 스택에 삽입되고 포커스를 가져간다. 이전 activity는 여전히 스택에 남아 있지만, stopped 상태로 들어간다. Activity가 stop되더라도 시스템에서 UI state를 보관한다. 새로 실행된 activity가 pop되면 이전 activity가 resume되고, 시스템이 보관하고 있던 UI state가 화면에 복원된다.
스택에 저장된 activity는 절대로 순서가 바뀌지 않고, push/pop 연산으로만 삽입/삭제될 수 있다. Activity가 실행/종료됨에 따라 스택의 상태를 그려보면 다음과 같다.
사용자가 계속 뒤로 가기를 누르면, 스택에 저장된 activity가 계속 pop된다. 모든 activity가 스택에서 pop되면 task도 종료된다.
Root launcher activity에서 뒤로 가기를 눌렀을 때
Root launcher activity는 ``ACTION_MAIN``과 ``CATEGORY_LAUNCHER`` intent filter를 모두 선언한 activity를 말한다. 앱의 진입점 역할을 하며, task를 시작할 때 사용될 수 있다.
<activity
android:name=".splash.SplashActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
Root launcher activity에서 뒤로 가기를 눌렀을 때의 동작은 안드로이드 버전마다 다르다.
- Android 11 이하: Activity가 완전히 종료된다.
- Android 12 이상: Activity가 종료되는 대신, 홈 버튼을 눌렀을 때처럼 백그라운드로 이동한다. 사용자가 앱을 다시 실행하고 싶을 때 빠르게 재시작할 수 있도록 백그라운드로 옮기는 것.
뒤로 가기 작업을 커스텀하고 싶다면, ``onBackPressed()``을 오버라이딩하는 대신 AndroidX Activity API를 활용하는 것이 좋다. 이 내용은 다음 글에서 알아보자.
물론 ``onBackPressed()`` 함수를 오버라이딩해도 된다. 하지만 이 함수를 오버라이딩하더라도 ``super.onBackPressed()`` 함수는 꼭 실행하자. 어쨌든 사용자가 나갈 수 있어야 하니까.
백그라운드/포그라운드 task
Task는 사용자가 새로운 task를 실행하거나 홈 화면으로 이동할 때 백그라운드로 갈 수 있다. 백그라운드에 있는 task의 모든 activity는 stop되지만, task의 back stack은 온전히 유지된다. 포커스만 잃을 뿐 데이터는 전혀 손실되지 않는다.
단, 백그라운드 task가 너무 많아서 메모리가 부족할 경우 시스템에서 몇몇 task 또는 activity를 destroy할 수 있다. Destroy된 activity의 데이터는 손실된다.
다음 그림처럼 task A가 백그라운드에, task B가 포그라운드에 있다고 해 보자.
이 상황에서 사용자가 홈 버튼을 누르면 task B가 백그라운드로 들어간다. Task A를 실행한 앱 아이콘을 터치하면, Task A가 다시 포그라운드로 돌아오고, back stack의 최상단에 있는 activity Y가 사용자에게 보이게 된다.
여러 개의 activity
위에서 back stack의 순서는 절대 바뀌지 않는다고 했다. 따라서 사용자가 앱을 사용하다 보면 같은 activity가 back stack에 여러 번 들어갈 수 있다. 물론 UI state는 따로따로 저장된다.
하지만 같은 activity가 back stack에 두 번 이상 들어가지 않게 할 수도 있다. 아래에서 다시 살펴보겠다.
멀티 윈도우
멀티 윈도우 환경에서는 윈도우마다 task가 따로 관리된다. 각 window는 여러 개의 task를 가질 수 있다.
Task 관리
안드로이드에서는 task의 모든 activity가 back stack으로 관리된다. 이 방법은 대부분의 경우에 효과적이므로, 평소에는 activity가 back stack에 어떻게 저장되는지 신경쓸 필요가 없다.
하지만 특별한 관리 방법이 필요할 때도 있다. 예를 들어 activity가 현재 task에서 실행되는 대신 새 task에서 실행되게 할 수도 있고, activity를 실행할 때 이미 존재하는 인스턴스를 포그라운드로 가져오고 싶을 수도 있고, 사용자가 task를 떠날 때 root를 제외한 모든 activity를 back stack에서 제거하고 싶을 수도 있다.
이런 작업은 manifest의 ``<activity>`` 태그와 ``startActivity()``에 전달하는 intent flag를 통해 수행할 수 있다.
Task와 관련된 ``<activity>``의 attribute는 다음과 같다.
- ``taskAffinity``
- ``launchMode``
- ``allowTaskReparenting``
- ``clearTaskOnLaunch``
- ``alwaysRetainTaskState``
- ``finishOnTaskLaunch``
Task와 관련된 intent flag는 다음과 같다.
Attribute와 flag를 사용하여 back stack의 동작을 바꾸는 방법에 대해 알아보자. 또, task와 activity가 최근 앱 화면에서 어떻게 관리되는지도 알아보자.
Launch mode 정의
Launch mode는 activity가 새로 만들어질 때 task와 상호작용하는 방법을 의미한다. Launch mode는 manifest와 intent flag로 정의할 수 있다.
Activity A가 B를 시작한다고 하면, B가 manifest에 정의한 launch mode와 A가 B에게 요청하는 launch mode가 존재할 수 있다. 만약 둘 다 존재한다면 A가 요청한 launch mode가 적용된다.
Manifest에서 정의할 수 있는 launch mode 중 intent flag로 정의할 수 없는 것도 있고, 마찬가지로 intent flag에서 정의할 수 있는 launch mode 중 manifest에서 정의할 수 없는 것도 있다.
Manifest로 launch mode 정의
Activity를 manifest에 등록할 때 activity가 task와 상호작용하는 ``launchMode`` 속성을 정의할 수 있다. 사용할 수 있는 값은 다음과 같다.
- ``"standard"``: 시스템 기본 모드이다. 현재 task에 새 activity가 push된다. Activity는 여러 번 만들어질 수 있으며, 만들어질 때마다 다른 task에 속할 수도 있다.
- ``"singleTop"``: 실행할 activity가 현재 task의 맨 위에 있다면, ``onNewIntent()`` 함수를 통해 해당 activity에 intent를 전달한다. Activity가 업데이트된다고 보면 된다. Task의 맨 위에 있지 않다면 ``"standard"``와 같다.
- ``"singleTask"``: 현재 task에 activity가 없다면, 새 task에 activity를 만든다. 현재 task에 activity가 있다면, 그 activity 위에 있는 모든 activity를 제거한 후 해당 activity에 intent를 전달한다(``onNewIntent()``). 다른 task에서 activity가 실행되고 있었다면, 해당 task를 포그라운드로 가져온다.
- ``"singleInstance"``: Task에 이 activity 하나만 들어갈 수 있다. 이 activity에서 실행하는 activity는 다른 task에 포함된다.
- ``"singleInstancePerTask"``: Activity가 task의 root로만 실행될 수 있다. 따라서 이 activity는 task마다 최대 하나만 존재할 수 있다. ``singleTask``와는 달리 ``FLAG_ACTIVITY_MULTIPLE_TASK`` 또는 ``FLAG_ACTIVITY_NEW_DOCUMENT`` flag를 설정하면 여러 task에서 activity를 실행할 수 있다.
Intent filter로 launch mode 정의
``startActivity()``를 통해 activity를 실행할 때, intent flag를 설정하면 task와 activity의 관계를 지정할 수 있다. 사용할 수 있는 플래그는 다음과 같다.
- ``FLAG_ACTIVITY_NEW_TASK``: ``"singleTask"``와 같다.
- ``FLAG_ACTIVITY_SINGLE_TOP``: ``"singleTop"``과 같다.
- ``FLAG_ACTIVITY_CLEAR_TOP``: 실행하려는 activity가 현재 task에서 이미 포함되어 있다면, 그 activity의 위에 있는 모든 activity를 destroy한다. 그 후 실행하려는 activity에 ``onNewIntent()``를 통해 intent를 전달한다. Manifest에서는 이 행동을 정의할 수 없다.
``FLAG_ACTIVITY_NEW_TASK``와 함께 사용되는 경우가 많다. 이 경우에는 실행하려는 activity를 포함하는 다른 task를 포그라운드로 가져온 후, 그 위에 있는 모든 activity를 종료한다.
Affinity 조작
Affinity는 activity가 선호하는 task를 의미한다. 일반적으로 같은 앱의 activity는 모두 같은 task에 속한다. 서로 다른 앱의 activity가 같은 affinity에 속할 수도 있고, 같은 앱의 activity가 서로 다른 affinity에 속할 수도 있다.
Affinity는 manifest의 ``<activity>`` 태그에서 ``taskAffinity`` 속성을 통해 설정할 수 있다. ``taskAffinity`` 속성은 ``<manifest>``에 정의된 패키지 이름이 아닌 문자열 값을 갖는다. 이 값이 activity의 기본 affinity로 사용된다.
Affinity를 사용하는 예시를 두 개 살펴보자. 우선 Activity를 시작할 때 ``FLAG_ACTIVITY_NEW_TASK`` 플래그를 설정한 경우이다.
일반적으로 새 activity는 자신을 실행한 task에서 실행된다. 하지만 ``FLAG_ACTIVITY_NEW_TASK`` 플래그를 설정하면, 시스템은 activity가 속할 task를 찾는다. 새 activity와 affinity가 같은 task가 있다면, 그 task에서 activity를 실행한다. Affinity가 같은 task가 없다면 새 task가 실행된다.
두 번째는 ``<activity>``의 ``allowTaskReparenting``가 ``"true"``로 설정된 경우이다. 이 경우에는 affinity가 같은 task로 activity가 이동할 수 있다.
예를 들어, 날씨 정보를 보여주는 activity가 여행 앱에 선언되어 있다고 하자. 다른 앱에서 날씨 정보 activity를 실행하면, 처음에는 다른 앱과 같은 task에서 실행된다. 하지만 여행 앱 task가 포그라운드로 실행되면, 날씨 정보 activity가 여행 앱이 속한 task로 옮겨갈 수 있다.
Back stack 비우기
사용자가 task를 오랫동안 실행하지 않으면, 시스템은 root를 제외한 모든 activity를 task에서 제거할 수 있다. 사용자가 task로 다시 돌아오면 root activity가 보인다. 사용자가 task를 오랫동안 실행하지 않았다면, 이전 task에서 작업하던 내용을 모두 잊었다고 가정하는 것.
이 행동을 수정할 수 있는 attribute가 있다.
- ``alwaysRetainTaskState``: ``"true"``로 설정되면 root를 포함한 모든 activity가 task에 보존된다.
- ``clearTaskOnLaunch``: ``"true"``로 설정되면 사용자가 task를 떠날 때마다 모든 activity가 제거된다. 사용자가 task에 돌아오면 root activity가 초기 상태로 실행된다. 즉 task를 아주 잠깐 떠났을 때에도 앱 상태가 초기화되는 것.
- ``finishOnTaskLaunch``: ``clearTaskOnLaunch``와 비슷하지만, task가 아닌 activity 단위에서 동작한다. 이 값이 ``"true"``로 설정되면 사용자가 task를 떠날 때 activity가 항상 종료된다. 단, root activity는 종료되지 않는다.
``clearTaskOnLaunch``와 ``finishOnTaskLaunch``는 ``FLAG_ACTIVITY_RESET_TASK_IF_NEEDED`` 플래그가 설정되지 않으면 무시된다.
Task 시작
Activity를 task의 시작점으로 만들려면 ``"android.intent.action.MAIN"`` action과 ``"android.intent.category.LAUNCHER"`` category를 선언해야 한다.
<activity ... >
<intent-filter ... >
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
...
</activity>
이 intent filter를 선언하면 앱 아이콘과 이름이 기기 화면에 보이며, 사용자가 앱을 실행하고 실행된 task에 돌아올 수 있는 기능을 제공한다.
Task로 돌아올 수 있는 기능이 중요하다. 사용자는 task를 떠난 후 앱 아이콘을 통해 언제든지 돌아올 수 있어야 한다. 따라서 task의 시작점 역할을 하는 activity는 ``"singleTask"`` 또는 ``"singleInstance"`` 중 하나의 값을 사용해야 한다.
이 intent filter를 선언하지 않으면, 기기 화면에 앱 아이콘이 보이지 않기 때문에 사용자가 task로 돌아갈 방법이 없어진다.
사용자가 activity로 돌아오지 못하게 하고 싶다면, ``finishOnTaskLaunch = "true"``를 사용하자. 이 값을 사용하면 사용자가 task를 떠날 때 activity가 항상 종료된다.
참고자료