launchMode 이해하기- singleTop, singleTask, singleInstance
안드로이드 개발에서 액티비티의 launchMode는 사용자 경험(UX)을 결정짓는 요소 중 하나이다.
사용자가 알림을 클릭했을 때 새 화면을 열지, 기존 화면을 갱신할지, 아니면 마치 홈 버튼을 누른 것처럼 동작할지 등 모든 흐름을 이 launchMode가 제어한다.
singleTop, singleTask, singleInstance의 미묘한 차이를 확실히 짚어보자.
먼저 태스크(Task)와 백스택(Back Stack)이라는 두 가지 핵심 개념을 이해해야 한다.
- Task: 사용자가 특정 작업을 수행하기 위해 상호작용하는 액티비티들의 그룹이다.
- Back Stack: 태스크 내의 액티비티들이 쌓이는 리스트로, 스택(LIFO) 구조로 관리된다. '뒤로 가기'를 누르면 가장 위에 있는(Top) 액티비티가 제거(pop)된다.
예를 들어, Task A: [A, B, C]는 태스크 A에 액티비티 A, B, C가 순서대로 쌓여있고, 현재 **C가 최상단(Top)**에 있어 사용자 눈에 보이는 상태를 말한다.
singleTop
singleTop은 이름 그대로 태스크 스택의 가장 위(Top) 하나만 확인한다.
실행하려는 액티비티가 이미 현재 태스크의 최상단(Top)에 존재한다면, 시스템은 새로운 인스턴스를 생성하지 않는다. 대신 기존 인스턴스를 재사용하며 onNewIntent() 메서드를 호출한다.
예시를 보자.
- 현재 상태:
Task A: [A, B](B는singleTop이며 현재 최상단에 있음) - 이벤트:
Intent(B)발생
시스템은 태스크 A의 꼭대기를 확인하고 "B가 이미 있네? singleTop이니까 재사용하자"라고 판단한다.
Task A: [A, B](스택 변화 없음). 대신 B의onNewIntent()가 실행된다.
singleTask (Clear Top)
singleTask는 태스크 내에서 자기 자신을 단 하나의 인스턴스로만 유지하려 한다. 'Clear Top'이라고 이해하면 쉽다.
실행하려는 액티비티가 태스크 내에 이미 존재한다면, 시스템은 해당 태스크(Task A)를 앞으로 가져온다(Foreground). 그리고 B 위에 쌓여있던 다른 액티비티들(C, D 등)을 모두 제거(Clear)해버리고 onNewIntent()를 호출한다.
이 부분은 조금 헷갈릴 수 있다.
- 현재 상태:
Task A: [A, B, C, D](B는singleTask로 설정됨) - 이벤트:
Intent(B)발생
시스템: "Task A에 B가 이미 있네. singleTask니까 위를 다 치워버리자(Clear Top)!"라고 판단하여 B 위에 있던 D와 C는 스택에서 제거된다(pop 및 onDestroy 호출).
Task A: [A, B](이제 B가 최상단이 되며,onNewIntent()가 호출된다).- A는 B보다 아래에 있었으므로 살아남는다.
singleInstance
singleInstance는 가장 특별하고 엄격한 모드다.
시스템 전체에서 단 하나의 인스턴스만 존재함을 보장하며, 아예 별도의 태스크에서 독립적으로 실행된다. 이 액티비티를 담고 있는 태스크는 절대 다른 액티비티를 담을 수 없는 독점 구역이 된다.
- 현재 상태:
Task A: [A, B] - 이벤트: 액티비티 B에서
Intent(Z)를singleInstance로 실행
시스템은 "Z만을 위한 완전히 새로운 태스크 B를 만들어야겠다"라고 판단한다.
Task A: [A, B](백그라운드로 이동)Task B: [Z](새 태스크가 포그라운드로 오고, Z가 생성됨)
Z를 위한 새 태스크 흐름이 생겼을 뿐, 기존 태스크 A는 죽지 않는다.
반대 상황이 헷갈릴 수 있다.
- 현재 상태:
Task B: [Z](Z가 최상단인 상태) - 이벤트: Z에서
Intent(C)를 실행 (C는 일반 액티비티)
시스템은 "C를 시작해야 하는데, Z가 있는 태스크 B는 독점 구역(singleInstance)이라 C를 넣을 수 없으니 C는 원래 있어야 할 태스크(Task A)에 넣어야겠다." 라고 판단한다.
그래서 시스템은 Task A를 다시 포그라운드로 가져와서 C를 실행한다.
Task B: [Z](백그라운드로 이동)Task A: [A, B, C](C가 생성되어 태스크 A 스택 위에 쌓임)
taskAffinity와의 관계
singleTask 역시 taskAffinity를 설정하면 새로운 태스크를 시작할 수 있기 때문에 singleInstance와 혼동하기 쉽다.
결정적인 차이는 '태스크가 시스템 전체에서 독점적인가(Exclusive)' 여부다.
singleTask라도 taskAffinity를 설정해서 새 태스크를 만들었다면, 그 안에서 "Task당 하나"라는 원칙만 지키면 된다. 즉, Z(singleTask)가 있는 새 태스크 B에서 C를 호출하면 [Z, C]처럼 같이 존재할 수 있다.
하지만 singleInstance라면, 태스크 B는 오직 [Z]만 가질 수 있다. Z에서 C를 호출하면 C는 무조건 다른 태스크로 쫓겨난다.
affinity를 보면 나의 경우 떠오르는 게 하나 있다.
finishAffinity()
finishAffinity()는 현재 액티비티와 같은 소속 이름을 가진, 이 태스크 내의 모든 부모 액티비티들을 한꺼번에 종료시킨다.
finish() 와finishAffinity()의 가장 쉬운 비교는 다음과 같다.
finish(): "나만 갈게."- 현재 보고 있는 액티비티 하나만 종료(pop)한다.
- 뒤로 가기 버튼을 한 번 누른 것과 같다.
finishAffinity(): "우리 같이 가자."- 현재 액티비티뿐만 아니라, 나를 불러냈던 이전 액티비티들 중 나와 같은
taskAffinity를 가진 애들을 싹 다 종료한다. - 결과적으로 해당 태스크가 비워지면서 앱이 꺼지는 효과를 낸다.
- 현재 액티비티뿐만 아니라, 나를 불러냈던 이전 액티비티들 중 나와 같은
동작 예시를 보면 좀 더 이해가 잘 된다.
[ 메인 A -> 목록 B -> 상세 C -> 설정 D ] 순서로 깊이 들어와 있다고 가정하자. (모두 기본 Affinity인 com.myapp 소속이다.)
설정 D 화면에서 메서드를 호출하면
finish()호출 시:- D만 사라진다.
- 화면에는 상세 C가 보인다.
finishAffinity()호출 시:- D가 사라지면서, 밑에 깔린 C, B, A를 확인한다.
- "어? 너네도 나랑 같은
com.myapp소속이네? 다 같이 종료." - [A, B, C, D]가 모두 스택에서 제거된다.
- 앱이 완전히 종료되어 홈 화면이 보인다.
가장 대표적인 사용처는 로그아웃이나 앱 종료 버튼이다.
사용자가 앱 깊숙한 곳(설정 화면 등)에서 "로그아웃"을 눌렀다고 가정해 보자. 로그아웃이 되었는데 사용자가 뒤로 가기를 눌렀을 때, 다시 로그인된 상태의 이전 화면(상세 페이지 등)이 나오면 보안상 문제가 되고, UX적으로도 어색한 상황이다.
이때 finishAffinity()를 쓰면 깔끔하다.
fun onLogoutClicked() {
finishAffinity()
}
로그인 액티비티를 따로 쓴다면, 인텐트 플래그 FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK를 써서 로그인 화면으로 갈아끼우는 방식을 쓰면 더 좋지만finishAffinity()로 현재 스택을 비우는 것도 방법 중 하나라고 할 수 있다.