본문 바로가기

Android/Tech

Service 를 운용하는 환경에서 앱을 완전히 종료하기

Unsplash, Kenny Orr.

동기

지난 여름에 개발했던 앱 <뮤런> 의 리팩토링이 한창입니다. 음악 플레이어 앱 특성 상, 음악을 재생 중이라면 앱이 종료되어도(된 것처럼 보여도) 플레이어의 상태가 유지되어야 합니다. 이러한 스펙을 구현하는 과정 중 학습했던 것을 기록합니다.

 


완전한 종료

 

공식 문서, Processes and app lifecycle 의 일부입니다. 다양한 앱 구성 요소의 수명 주기에 대한 이해가 부족하면 중요한 작업 도중 앱 프로세스가 종료될 수 있다는 내용인데, 이는 곧, 다양한 앱 구성 요소의 수명 주기에 대한 이해가 부족하면 앱 프로세스를 종료하고 싶어도 종료하지 못할 수 있다는 것을 의미하기도 합니다.

 

앱을 완전히 종료해야 하는 이유는 간단합니다. 나도 모르게 무한한 작업이 프로세스 내에서 진행되고 있거나, 미처 해제하지 못한 리소스가 존재하는 경우가 있을 수 있기 때문입니다. 이는 곧 디바이스 리소스의 낭비로 이어집니다.

 

 

Activity  |  Android Developers

android.inputmethodservice

developer.android.com

 

위 문서에 Empty Process 라는 용어가 나오는데요. 이는 최소한의 앱 캐싱이며, 디바이스 퍼포먼스와 반응성을 위한 OS 수준의 최적화 입니다. 동작하는 앱 컴포넌트가 존재하지 않는 상태입니다. 즉, 앱이 최소한 Empty Process 상태가 되어야 완전한 종료라고 볼 수 있는 것입니다.

 


우리가 신경 써야 할 것

앱을 제대로 종료하기 위해 우리가 신경 써야 할 것은 Activity 와 Service 에 의해 종료가 지연될 수 있는 프로세스의 수명 주기입니다. 프로세스 수명 주기의 경우, 다음 공식 문서에서 자세히 설명하고 있습니다.

 

 

프로세스 및 애플리케이션 수명 주기  |  Android 개발자  |  Android Developers

대부분의 경우 모든 Android 애플리케이션은 자체 Linux 프로세스에서 실행됩니다. 이 프로세스는 일부 코드를 실행해야 할 때 애플리케이션용으로 생성되며 더 이상 필요하지 않고 시스템이 메모

developer.android.com

 

해당 문서에서는 네 단계의 우선 순위로 나누어진 프로세스 상태에 대해 설명하고 있습니다. 화면에 표시되는 Activity 가 없고 Service 를 운용하는 경우, 2번 Visible Process 또는 3번 Service Process 에 속하게 될 겁니다.

Visible Process

다음 3가지 조건 중 하나라도 해당된다면, 해당 프로세스는 Visible Process 입니다.

  1. 특정 다이얼로그가 표시되어 화면을 점유하고, 그 아래에 Activity 가 표시되는 경우(대표적으로 권한 요청)
  2. ForegroundService 가 존재하는 경우
  3. 라이브 배경화면 또는 InputMethodService 등을 호스팅하는 경우

Visible Process 의 경우, 매우 중요한 프로세스로 인식되어 시스템에 의해 종료되지 않습니다. 즉, 메모리 부족으로 인한 시스템의 강제 종료로부터 다소 안전하다고 볼 수 있습니다.

Service Process

VisibleProcess 가 아니면서 BackgroundService 가 운용되고 있는 경우를 말합니다. 사용자에게 표시되지는 않지만 작업이 진행 중인 경우입니다. 이 역시도 다소 안전하긴 한데, 30분 이상 운용된 Service 만이 존재하는 프로세스는 시스템에 의해 Cached Process 가 될 수 있습니다. 즉, 30분 이상 작업을 수행할 거라면 JobScheduler 등을 이용하거나, ForegroundService 로 만들면 됩니다.

 

Visible ProcessService Process 는 메모리 부족으로 인한 시스템의 강제 종료로부터 안전하다는 이야기를 했는데, 우리는 이들이 안전한 것에 집중해야 합니다. 안전하다는 것은 결국 쉽게 종료되지 않는다는 것을 의미하기 때문입니다.

 

정리하자면, ForegroundService 가 운용되고 있다면 앱은 완전히 종료될 수 없습니다. (메모리가 극도로 부족한 경우 제외) BackgroundService 가 운용되고 있다면 최소 30분 이상은 지나야 완전히 종료될 수 있습니다.

 

즉, 앱을 완전히 종료하려면 모든 Service 가 종료되어야만 합니다.

 


Service 의 세 가지 종류

Service 는 생성 방식에 따라 세 가지 종류로 나뉩니다. 또한 큰 차이가 존재하는데, 바로 시스템이 인식하는 중요도의 차이입니다. 중요도가 높은 순서대로 나열합니다.

1. ForegroundService

먼저, 안드로이드 Oreo 버전 이후부터 사용되는 ForegroundService 입니다. 생성 시, Intent 를 인자로 받는 Context.startForegroundService() 메서드를 호출하면 됩니다. 정상적인 실행을 위해 Notification 이 표시되어야 하므로, Service.startForeground() 메서드를 실행시켜 Notification 을 표시해주면 됩니다. 음악 재생, 위치 추적, 네트워크 I/O, 파일 I/O 등 다양하게 사용될 수 있습니다.

 

앞서 기술하였듯, ForegroundService 가 실행되고 있는 것만으로도 Visible Process 의 조건을 충족할 수 있습니다. 즉, ForegroundService 가 실행되고 있다면 어지간한 경우에는 앱이 완전히 종료될 수 없다는 것을 의미합니다.

 

그러므로, ForegroundService 를 운용하는 앱을 종료하기 위해서는 Context.stopService() 메서드를 호출하거나, 해당 Service 내에서 stopSelf() 메서드를 호출하여, Service 가 프로세스에서 해제될 수 있도록 하여야 합니다.

 

stopForeground() 메서드도 존재하는데, 이는 해당 Service 를 다음에 설명할 일반적인 BackgroundService 로 전환합니다.

2. BackgroundService

일반적인 Service 입니다. Context.startService() 메서드로 실행되며, 현재로써는 사용을 지양하는 것이 좋습니다. 안드로이드 Oreo 버전 이전에 사용되던 방식이며, 현재는 호환성 유지를 목적으로 남아 있습니다. 물론 사용하는 데에 큰 지장은 없으며, 여전히 자주 사용됩니다.

 

Context.startService() 로 실행된 Service 만 운용되고 있는 경우, 시스템은 이를 Service Process 로 간주하고 30분 이상이 흐르면 이를 Cached Process 로 강등시키려 할 수 있습니다. 그러므로, 이 역시 ForegroundService  와 같이 Context.stopService() 또는 stopSelf() 메서드를 통해 Service프로세스에서 해제해줘야 앱을 완전히 종료할 수 있습니다.

3. BoundService

바인딩된 서비스를 말합니다. Context.bindService() 메서드를 통해 생성되며, 생성의 책임을 수행한 컴포넌트와 직접적으로 상호 작용할 수 있도록 ServiceConnection 객체를 제공할 수 있습니다. 이를 통해 두 컴포넌트는 Client-Server 와 같은 관계를 갖게 됩니다. BoundService 의 메서드를 자유롭게 호출할 수 있습니다.

 

BoundService 는 생성의 책임을 수행한 Context 에 바인딩되어, 해당 Context 의 수명 주기를 함께 합니다. 즉, 특정 Activity 에서 Service 를 바인딩하면, 해당 Activity 가 소멸될 때 Service 의 바인딩도 해제되며 해당 Service 가 소멸됩니다.

 

즉, 별도의 Service 에서 bindService() 메서드를 통해 바인딩한 Service 가 아니라 Activity 에서 바인딩한 경우, 해제에 큰 신경을 쓰지 않아도 됩니다.

 

하나의 BoundService 에 여러 Context 를 연결할 수도 있는데, 이 경우, 바인딩된 모든 Context 가 소멸되면 그 때 Service 도 소멸됩니다.

 


결론

모든 앱은 디바이스 리소스 보존을 위해 정상적으로 완전히 종료되어야만 합니다.

Service 를 사용하는 앱들의 경우, Service 에 대한 모든 종료가 보장되어야만 합니다.

 

Context.startForegroundService() 메서드로 실행했다면, Context.stopService() 또는 stopSelf() 를 통해 Service 를 꼭 종료해 주세요. Context.startService() 의 경우에도 마찬가지입니다.

 

Context.bindService() 메서드로 실행했다면, Context 가 어떤 컴포넌트의 Context 인지가 중요합니다.

Activity 라면 크게 신경 쓸 것이 없고, Service 라면 해당 Service 를 잘 종료해 주세요.


프로덕트 레벨이 아닌 사이드 프로젝트 앱들의 경우, Service 나 BroadcastReceiver 등에 대해 깊게 활용할 일이 잘 없는 것 같습니다. (적어도 제 수준에서는요.) 그러다보니, 너무나 중요한 개념들인데도 불구하고 학습에 소홀하게 되는 것 같아요. 이번 기회에 시스템에서의 프로세스 중요도, 프로세스 상태, 프로세스 수명 주기 등 다양한 지식도 함께 얻을 수 있어 좋았습니다.