저는 안드로이드 개발을 하면서 에뮬레이터, 실제 디바이스를 오가면서 앱을 테스트하고 있어요. 디바이스의 경우에는 패드와 저/고사양 기기로 총 3가지를 사용하는데요. 그중에서 저사양 기기에서 앱이 재시작 되는 로직에 프레임드랍이 생기거나 재시작이 무시되는 경우가 종종 발견됐습니다. 처음엔 단순히 기기 성능 문제라고 생각했습니다. 그러나 충분히 최적화 가능하지 않을까? 라는 생각으로 코드를 뜯어보니 사용하던 ProcessPhoenix 라이브러리가 생각보다 무거운 작업을 하고 있었습니다.
좀 무거운듯
하이링구얼 프로젝트에서는 토큰 만료, 로그아웃, 회원탈퇴의 경우 앱을 재시작하고 있습니다. 그래서 ProcessPhoenix라는 라이브러리를 사용했습니다. 많은 안드로이드 프로젝트에서 사용하는 검증된 라이브러리였으니까요.
// 기존 코드, 아주 간단하다
override fun restartApp() {
ProcessPhoenix.triggerRebirth(context)
}
단 한 줄로 앱 재시작을 구현할 수 있으니 편리했습니다. 하지만 이 편리함 뒤에는 우리가 모르던 비용이 숨어 있었어요.
ProcessPhoenix가 하는 일
라이브러리 내부를 들여다보니 재시작을 위해 별도의 프로세스 :phoenix를 생성하고 있었습니다. 앱을 종료한 뒤 새 프로세스가 다시 앱을 실행시키는 방식입니다.
멀티 프로세스 생성은 메모리 오버헤드를 동반합니다. 특히 RAM이 부족한 저사양 기기에서는 시스템이 새 프로세스 생성을 거부하거나 기존 프로세스를 강제 종료할 수 있어요. 실제로 저사양 기기의 경우 하드웨어 스펙은 4GB였지만 실제 여유공간은 평균 1~2GB였습니다.
단순히 앱을 재시작하는 기능에 멀티 프로세스가 필요할까요? 우리가 원하는건 메모리 상의 앱 데이터와 액티비티를 재시작하는건데… 때문에 가볍게 만들기 위해서 라이브러리를 제거하고 네이티브 API로 직접 구현해보기로 했습니다.
네이티브 API로 다시 만들기
안드로이드는 앱 재시작을 위한 표준 방법을 제공합니다. Intent로 런처 액티비티를 다시 시작하고 기존 태스크를 정리하면 됩니다.
override fun restartApp() {
val intent = context.packageManager
.getLaunchIntentForPackage(context.packageName)
?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
} ?: return
context.startActivity(intent)
killCurrentProcess()
}
private fun killCurrentProcess() {
Process.killProcess(Process.myPid())
exitProcess(0)
}
코드는 조금 길어졌지만 동작은 명확합니다.
- 현재 앱의 런처 인텐트를 가져옵니다
FLAG_ACTIVITY_NEW_TASK와FLAG_ACTIVITY_CLEAR_TASK플래그로 기존 태스크를 정리합니다- 새로운 액티비티를 시작합니다
- 현재 프로세스를 종료합니다
별도의 프로세스를 만들지 않고 단일 프로세스 내에서 모든 작업이 처리됩니다. 라이브러리 의존성도 사라졌어요.
Compose에 맞는 아키텍처 적용하기
네이티브 구현으로 바꾸면서 UI 레이어의 구조도 개선했습니다. 기존에는 각 화면에서 Context를 직접 참조했어요.
// 기존 방식
val context = LocalContext.current
// ...
ProcessPhoenix.triggerRebirth(context)
UI 컴포넌트가 Context와 외부 라이브러리에 직접 의존하는 구조였습니다. 또한 UI모듈에서 앱을 재시작하기 위해서는 ProcessPhoenix 의존성을 꼭 추가해야 하는 구조기도 해요.
CompositionLocal 도입
CompositionLocal을 사용해서 UI 컴포넌트가 추상화된 인터페이스에만 의존하도록 바꿨습니다.
val LocalAppRestarter = staticCompositionLocalOf<AppRestarter> {
error("No AppRestarter provided")
}
이제 MainActivity에서 Hilt로 주입받은 구현체를 제공하고 각 화면에서는 LocalAppRestarter.current로 접근합니다.
// UI 레이어
val appRestarter = LocalAppRestarter.current
// ...
is MyPageSideEffect.RestartApp -> appRestarter.restartApp()
Context 의존성이 사라지면서 UI 컴포넌트는 순수한 인터페이스에만 의존하게 되었어요. 추후 테스트 혹은 다른 구현체로의 교체도 수월해졌습니다.
생각보다 간단했던 것들
처음엔 "검증된 라이브러리를 왜 굳이 직접 만들어?" 하는 의구심도 있었습니다. 하지만 막상 구현해보니 생각보다 간단했어요.
사실 외부 라이브러리를 사용할 때는 내부 구현을 자세히 살펴보지 않게 됩니다. ProcessPhoenix를 도입할 때도 "재시작이 잘 되네"라는 결과만 확인했지 멀티 프로세스를 생성한다는 구현 방식까지는 신경 쓰지 않았어요.
반면 직접 구현한 코드는 모든 동작이 눈에 보입니다. Intent 생성, 플래그 설정, 프로세스 종료까지 각 단계가 무엇을 하는지 명확해요. 문제가 생겼을 때 디버깅하기도 훨씬 쉽습니다.
의존성에 대해 다시 생각하다
이번 작업을 하면서 외부 라이브러리를 도입할 때 고민해야 할 질문들이 생겼습니다.
이 라이브러리가 정확히 무엇을 하는가?
단순히 "앱을 재시작한다"는 추상적인 기능만 알고 있었습니다. 멀티 프로세스를 생성한다는 구체적인 구현 방식은 몰랐습니다.😅 앞으로는 간단하게라도 라이브러리를 도입하기 전에 내부 구현을 살펴보는 습관을 만드려고 합니다.
이 기능을 직접 구현하기 어려운가?
ProcessPhoenix가 제공하는 기능은 안드로이드 네이티브 API로도 충분히 구현 가능했습니다. 20줄 정도의 코드로 같은 기능을 만들 수 있다면 라이브러리가 필요한지 고민해도 좋을 것 같아요.
유지보수 비용은 어떤가?
외부 라이브러리는 사용하는 개발자가 통제할 수단이 한정적이거나 없습니다. AGP 9.0 업데이트를 하면서 Hilt가 호환성 문제를 겪었던 것처럼요. 하지만 직접 구현한 코드는 우리가 완전히 통제할 수 있습니다. 로그를 언제든 추가할수 있고 로직을 완전히 제어 할 수 있습니다.
앞으로 확인해야 할 것
지금까지는 가설과 코드 분석에 기반한 개선이었습니다. 직접 확인했을때는 앱의 동작도 문제가 없었고 체감상 속도가 개선됐고 프레임 드랍이 사라졌지만 실제로 성능이 개선되었는지 확인하려면 데이터가 필요합니다.
측정해야 할 지표
- 저사양 기기에서 재시작 시 멈춤 현상 발생 빈도
- 시스템에 의한 강제 종료 발생 빈도
- 재시작 완료까지 소요 시간
- 메모리 사용량 변화
이 데이터를 수집하면 ProcessPhoenix 제거가 실제로 효과가 있었는지 확인할 수 있습니다. 특히 2GB 이하 RAM 기기에서의 개선 정도가 궁금하네요.
마치며
"편리하다"는 이유만으로 라이브러리를 도입하면 그 비용을 놓치기 쉽습니다. 이번 작업을 통해 외부 의존성을 도입할 때 더 신중하게 생각하게 되었어요.
모든 라이브러리를 직접 구현하자는 이야기는 아닙니다. 복잡한 기능이나 검증이 필요한 영역에서는 라이브러리를 잘 활용하는것이 좋다고 생각해요. 다만 그 라이브러리가 무엇을 하는지 우리가 직접 만들 수 없는 것인지를 먼저 생각해보면 좋을 것 같습니다. 가끔은 라이브러리를 걷어내는 것도 좋은 리팩터링이라고 생각해요 🙂
오늘도 긴 글 읽어주셔서 감사합니다. 같은 문제가 있었다면 도움이 되셨으면 좋겠어요!
'Android > Compose' 카테고리의 다른 글
| ??님 Preview가 흐릿한데 왜그래요? (0) | 2026.03.08 |
|---|---|
| DisposableEffect대신 LifecycleEffect (0) | 2026.01.19 |
| Compose에서 골치덩어리 EdgeToEdge를 잘 써보자. (1) | 2025.09.23 |
| 오픈소스 생태계에서 오픈소스 명시하기 (0) | 2025.09.23 |
| StateFlow를 활용한 네비게이션 상태 관리와 UI 로직 분리 (1) | 2025.08.09 |