티스토리 뷰
안녕하세요. Mobile Application 팀 전계원입니다.
QA 과정에서 발견된 이슈를 분석하던 중 원인이 OS 에 있어서 발생한 이슈였던 경험이 있었습니다.
원인이 OS 자체에 있다는 점이 신기했던 면도 있었지만, 해당 버그가 OS 10 이하를 지원하는 앱이라면 누구나 겪을 수도 있다는 사실이 인상 깊었습니다.
그래서 많은 분들께 공유드리고자 버그의 원인을 분석하고 해결방안을 찾아갔던 그 당시의 이야기를 작성하였습니다.
0. 프롤로그 - 버그 발견
G마켓에는 QA 부서가 별도로 존재합니다.
그리고 어느 날 QA 담당자분을 통해 재연영상과 함께 이슈를 할당받았습니다.
무엇이 문제인지 알 것 같나요?? 천천히 영상의 상황을 살펴보겠습니다.
옥션에서 새로운 페이지로 이동하였습니다.
그리고 정상적인 상황이라면 옥션 화면을 백그라운드로 보낸 후 다시 옥션 앱에 접속하게 되면 마지막으로 접속했던 화면이 나타나야 합니다.
하지만 옥션 런쳐 아이콘을 클릭하여 옥션 앱에 다시 접속 해보니, 마지막 접속했던 화면이 아닌 홈 화면이 나타나는 오류가 발생하였습니다.
그런데 더욱 혼란스러웠던 점은
될 때도 있고, 안 될 때도 있다는 점 입니다.
대체 왜 이런 버그가 발생한 것일까요??
1. 분석 - 버그 재연경로 탐색
1-1. 실마리
영상 내용을 재연하기 위해 테스트를 반복하다가 한 가지 실마리를 알게 되었습니다.
- "내 파일" 앱 접속
- 테스트용 옥션 apk 파일 설치
- 설치 완료 시 나타나는 "실행하시겠습니까?" 창에서 "예" 선택하여 앱 실행
- 영상 속 재연경로를 따라 재연
위와 같이 "내 파일" 앱에서 옥션을 실행하는 과정을 통하면 버그가 무조건 발생한다는 점을 확인하였습니다.
반면, 이미 설치되어 있는 바탕화면의 옥션 아이콘을 통해 앱에 접속하여 재연을 하고자 하면 정상동작하였습니다.
1-2. 증명 - '내 파일' 뿐만 아니라 다른 외부 앱에서 실행하여도 발생하는 버그이다.
두 시나리오는 "내 파일"을 통한 실행과 런쳐 아이콘을 클릭하여 실행하는 차이만 있습니다.
그렇기에 "내 파일" 이라는 앱의 문제인 것인지, 옥션을 외부 앱에서 실행시키는 과정에서 생긴 문제인지 알아야 했습니다.
옥션을 실행시킬 수 있는 외부 앱이 "내 파일" 말고 다른 앱이 있는지 생각해 보니 "플레이 스토어" 앱이 있었습니다.
그래서 "플레이스토어" 앱을 통해 옥션을 실행해 보았을 때에도 동일한 결과가 나타나는지 테스트를 진행해 보았습니다.
* 런쳐 아이콘 : 기기 바탕화면에 설치되어 있는 옥션 아이콘
위와 같은 결과로 미루어보았을 때 외부 앱에서 실행하는 방법과 아이콘을 클릭하여 실행하는 방법이 교차되는 것이 원인에 큰 영향을 미치지 않을까 짐작하였습니다.
1-3. 증명 - 외부 앱에서의 실행으로 인해 발생하는 버그이다
이번엔 샘플 코드를 작성하여 정말 외부 앱에서의 실행이 원인인지 체크해 보았습니다.
// com.example.myapplication3.MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.button1).setOnClickListener {
Intent(this, SecondActivity::class.java).let {
startActivity(it) // send to secondActivity
}
}
}
}
// com.example.myapplication3.SecondActivity.kt
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_second)
}
}
<!-- com.example.myapplication3 의 AndroidManifest.xml -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.myapplication">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MyApplication3"
tools:targetApi="31">
<activity
android:name=".SecondActivity"
android:exported="false" />
<activity
android:launchMode="singleTop"
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
// com.example.outerapplication.MainActivity.kt
class MainActivity : AppCompatActivity() {
val newAppPackageName = "com.example.myapplication3"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.button).setOnClickListener {
packageManager.getLaunchIntentForPackage(newAppPackageName)?.let {
startActivity(it)
}
}
}
}
<!-- com.example.outerapplictaion 의 AndroidManifest.xml -->
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.outerapplication">
<queries>
<package android:name="com.example.myapplication3" />
</queries>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.OuterApplication"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
위와 같이 코드를 작성하고, OS 10 이하의 에뮬레이터와 OS 11 이상의 애뮬레이터의 결과를 비교하니 동일하게 버그가 발생하는 것을 확인하였습니다.
그래서 버그현상은 외부 앱에서 실행하는 방법과 아이콘을 클릭하여 실행하는 방법이 교차되었을 때 backstack에서 문제가 발생하는 것임을 알 수 있었습니다.
2. 분석 - ADB 로 backstack 상황 분석
2-1. 방법
재연경로를 찾았으니 이제 원인을 분석해 보겠습니다.
Android Studio 의 Terminal 에서
adb shell dumpsys activity
명령어를 터미널에 입력하면 작동 중인 Emulator 의 현재 Backstack 을 확인할 수 있습니다. (adb 가 설치 되어있어야 합니다)
ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
Display #0 (activities from top to bottom):
Stack #1:
mFullscreen=true
mBounds=null
...
Running activities (most recent first):
TaskRecord{35bfc87 #104 A=com.example.myapplication3 U=0 StackId=1 sz=2}
Run #2: ActivityRecord{7d3213e u0 com.example.myapplication3/.SecondActivity t104}
Run #1: ActivityRecord{69508c u0 com.example.myapplication3/.MainActivity t104}
TaskRecord{c85d7b4 #103 A=com.example.outerapplication U=0 StackId=1 sz=1}
Run #0: ActivityRecord{f1252fb u0 com.example.outerapplication/.MainActivity t103}
mResumedActivity: ActivityRecord{7d3213e u0 com.example.myapplication3/.SecondActivity t104}
출력되는 로그의 Running activities 부문을 확인해 보면, 위와 같이 myapplication3 과 outerapplication 두 개의 앱이 열려있고, 그중 myapplication3 에는 backstack 에 MainActivity 와 SecondActivity 가 쌓여있는 것을 확인할 수 있습니다.
이러한 방법으로 backstack 에 어떤 문제가 있었는지 분석해 보겠습니다.
2-2. 확인 결과
위와 같이 오류동작 시나리오를 재연하면 OS10 이하에서 다시 앱으로 돌아왔을 때 backstack에 Activity 가 존재하여도 마치 처음 시작하는 것처럼 Launcher Activity(FirstActivity)가 실행되어 backstack 에 적재되는 것을 알 수 있었습니다.
코드상으로 OS 10 이하 버전에 대해 특별한 처리를 하지 않았음에도 이와 같은 차이를 보여주었기에 OS 에서 발생한 버그인 것으로 짐작할 수 있었습니다.
3. 분석 - 시스템 버그 히스토리 분석 (feat. IssueTracker)
backstack에 Launcher Activity 가 쌓이는 오류동작을 가지며, 이는 OS 상에서의 문제임을 확인했습니다.
이제 이와 유사한 해결사례가 없었는지 확인해보니 IssueTracker 에 동일한 이슈가 제보되었던 흔적을 발견하였습니다.
2009년 4월 7일 issue tracker 제보글 : https://issuetracker.google.com/issues/36907463 => Won't Fix (Obsolete)
재미있게도 이러한 버그 현상에 대해서는 2009년에 처음 제보되었었습니다.
2009 년에는 "이클립스' IDE 에서 실행 후 바탕화면 갔다가 다시 아이콘을 눌러 접속할 때 backstack 에 Activity 가 중첩되어 쌓이는 이슈가 발견되었다고 제보되었지만 아무런 코멘트 없이 Won't Fix 처리되었었습니다.
2012년 3월 10일 issue tracker 제보글 : https://issuetracker.google.com/issues/36941942 => Won't Fix (Obsolete)
2012 년에는 마켓앱을 통해 앱을 실행시켰을 때 backstack 에 Activity 가 중첩되어 쌓이는 이슈가 발견되었다고 다시 제보되었으나 역시 아무런 코멘트 없이 Won't Fix 처리되었습니다.
2017년 7월 27일 issue tracker 제보글 : https://issuetracker.google.com/issues/64108432 => Fixed
해결되지 않은 채 이어지다가 2017년에 12년도에 생성했던 이슈와 같은 내용으로 이슈가 재생성되었습니다.
comment #14 (Sep 27, 2019 04:50PM)
Marked as fixed.
Thanks for reporting this issue. The issue has been fixed and it will become available in a future Android release.
결국 2019년 9월 27일에 해당 이슈가 해결되었으며, 해결된 내용은 이후의 안드로이드 릴리즈 버전에 적용될 예정이라는 구글 담당자분의 코멘트가 첨부되었습니다.
그래서 2019년 9월 27일의 이후에 출시된 버전인 Android 11 에서부터는 이와 같은 버그가 발생하지 않았던 것이었습니다.
4. 대응방법
해당 버그에 대한 히스토리를 알게 되었으니, 이제는 OS 10 이하 앱에서 해당 버그를 대응하는 방법에 대해 알아보겠습니다.
버그 상황은 마지막에 접속했던 화면이 나오지 않고, 기존 backstack 에 MainActivity 가 쌓이는 버그입니다. 이를 다르게 해석하면, backstack 에 다른 Activity 정보가 쌓여있지만 마치 처음 시작하는 것처럼 Intent.CATEGORY_LAUNCHER 와 Intent.ACTION_MAIN 플래그와 함께 backstack 위에 새로운 MainActivity 를 실행하는 것이 현재 상황의 문제점이었습니다.
그렇기에 Intent.CATEGORY_LAUNCHER 와 Intent.ACTION_MAIN 플래그와 함께 MainActivity 에 진입하였을 때 현재의 Activity 가 taskRoot 가 아니면 위 문제 상황이 발생한 것으로 인지하고 finish() 처리를 통해 Activity 를 종료하면서, 가장 최근 backstack에 남 아있는 Activity 가 포그라운드로 올라오도록 할 수 있습니다.
결국 사용자 입장에서는 버그가 발생하여도 backstack 최상단에 존재하는 마지막으로 진입했던 화면(Activity)이 보여지고 이는 화면상으로 OS 11 이상과 동일하게 동작하기에 이와 같은 방법으로 대응할 수 있습니다.
5. TL;DR
OS 10 이하에서 외부 앱에서 실행하는 방법과 아이콘을 클릭하여 실행하는 방법이 교차되었을 때 backstack에 Activity 가 존재함에도 Launcher Activity 가 실행되는 문제가 발생합니다.
이에 대한 원인은 OS 에 있었으며, Issue Tracker 에 제보 및 해결되어 OS 11 부터는 이와 같은 문제가 발생하지 않습니다.
OS 10 이하에서는 Category 가 Intent.CATEGORY_LAUNCHER 이고, intent.action 이 ACTION_MAIN 일 때 isTaskRoot 값을 확인 후 처리하는 방법으로 버그에 대한 대응을 할 수 있습니다.
6. 참고자료
Issue Tracker : https://issuetracker.google.com/issues/36941942
'Mobile' 카테고리의 다른 글
jcenter, 이제 문 닫습니다 (3) | 2024.07.17 |
---|---|
statements 가 있는 switch/when 구문 deep dive (feat. bytecode) (0) | 2023.10.25 |
Xcode 15 의 iOS 17 빌드에서 User Agent 가 원하는 값으로 설정되지 않을때 (feat. iOS 버전별 WebKit 버전과 작업 내역 확인하기) (2) | 2023.10.19 |
DiffUtil 이해하기 (0) | 2023.05.17 |
버그와 함께 알아보는 RecyclerView 에서 wrap_content 사용을 조심해야 하는 이유 (0) | 2023.04.05 |