[Android Fundamentals] Service - 1. Overview
Service는 사용자와 상호작용하지 않는 긴 작업을 표현하거나, 다른 앱에게 기능을 제공하는 컴포넌트이다. 서비스를 선언한 후에는 manifest의 ``<service>``에 서비스에 대한 정보를 작성해야 한다. 서비스는 ``Context.startService()`` 또는 ``Context.bindService()``로 작성해야 한다.
다른 컴포넌트와 마찬가지로 서비스도 프로세스의 메인 스레드에서 실행된다. 따라서 서비스에서 음악 재생 같은 CPU 작업이나 네트워크 같은 blocking 작업을 수행할 때에는 별도의 스레드를 만드는 것이 좋다. 안드로이드에서는 별도의 스레드를 갖는 서비스의 표준 구현으로 ``JobIntentService``를 제공한다.
서비스란?
앞서 말했듯이 서비스는 어떤 작업 또는 다른 앱에게 제공하는 진입점 역할을 한다. 이런 특성 때문에 서비스에 대한 오해가 자주 생긴다.
- 우선, 서비스는 별도의 프로세스가 아니다. 서비스는 별도로 설정하지 않는 한 서비스를 만든 프로세스에서 함께 실행된다.
- 서비스는 스레드가 아니다. 서비스 자체로는 별도의 스레드를 갖지 않는다. 그래서 서비스가 CPU를 많이 사용하면 UI가 반응하지 않는 등의 문제가 생길 수 있다.
서비스는 프로세스도 스레드도 아니며, 두 가지 기능을 제공하는 아주 간단한 객체이다.
- 앱이 백그라운드에서 하고 싶은 작업을 시스템에게 설명하는 수단이다. ``백그라운드``를 좀 더 설명하면 앱이 사용자와 직접 상호작용하고 있지 않을 때에도 작업을 수행하고 싶다는 의미이다. ``Context.startService()``를 호출하여 시스템에게 서비스 할당 요청을 보낼 수 있다. Start된 서비스는 다른 컴포넌트가 종료하거나 서비스 스스로 종료할 때까지 실행된다.
- 다른 앱에게 기능을 제공하는 역할을 한다. ``Context.bindService()`` 함수에 대응한다. Bind된 서비스는 자신을 bind한 컴포넌트와 계속 연결되어 있다.
시스템은 단순히 서비스 객체를 만들고, ``onCreate`` 등의 콜백을 메인 스레드에서 실행한다. 별도의 스레드가 필요하다면 서비스에서 직접 만들어야 한다.
서비스 생명주기
서비스는 ``Context.startService()`` 또는 ``Context.bindService()``로 시작할 수 있다. 서비스를 시작한 컴포넌트를 클라이언트라고 지칭하겠다.
startService()
클라이언트가 ``Context.startService()``를 호출하면, 시스템은 서비스 객체를 만들고(이미 객체가 만들어져 있다면 해당 객체를 사용) 클라이언트가 매개변수로 전달한 값을 사용하여 서비스의 ``onStartCommand``를 호출한다. 서비스가 만들어진 후에는 ``Context.stopService()``가 호출되거나 스스로 ``stopSelf()``를 호출하기 전까지 계속 실행된다.
``Context.startService()``를 여러 번 호출해도 서비스 객체는 하나만 만들어진다. 따라서 ``Context.startService()``의 호출 횟수와 상관없이 ``Context.stopService()``를 한 번만 호출하면 서비스가 종료된다.
클라이언트가 보낸 요청 중 아직 ``onStartCommand``에 전달되지 않은 요청이 있을 때에는 서비스를 종료하지 말아야 할 수도 있다. ``stopSelfResult(startId: Int)``를 호출하면 클라이언트가 마지막으로 보낸 요청이 ``startId``일 때에만 서비스를 종료할 수 있다. (이 부분은 원문을 보는 게 좋을 듯)
Start된 서비스는 두 가지 모드로 실행될 수 있다. ``START_STICKY`` 모드를 적용하면 서비스가 명시적으로 stop될 때까지 계속 실행되며, ``START_NOT_STICKY`` 또는 ``START_REDELIVER_INTENT`` 모드를 적용하면 요청을 처리할 때에만 실행된다.
bindService()
서비스와의 연결이 필요한 경우에는 ``Context.bindService()``를 호출할 수 있다. 마찬가지로 서비스 객체를 만들거나(``onCreate()``도 호출될 수 있음) 기존 객체를 사용하지만, onStartCommand 대신 ``onBind(Intent): IBinder``를 호출한다. 클라이언트는 onBind 함수가 반환하는 ``IBinder`` 객체를 받는다. ``IBinder``는 안드로이드에서 remotable object와 메시지를 주고받을 때 사용하는 인터페이스이다.
서비스는 연결이 유지되는 한 계속 실행된다.
서비스를 start하면서 동시에 bind된 연결을 만들 수도 있다. startService()와 bindService()의 종료 조건 중 하나가 만족되면 서비스가 종료된다. 서비스가 종료될 때 ``onDestroy()`` 함수에서 모든 리소스를 해제해야 한다.
서비스 생명주기
시스템은 서비스가 start되었거나 bind된 연결이 있을 때 서비스를 최대한 오래 실행하려 한다. 메모리가 부족한 상황에서는 우선순위가 가장 낮은 서비스가 종료될 수 있다.
- 서비스의 ``onCreate()``, ``onStartCommand()``, ``onDestory()``가 실행되고 있다면, 서비스를 실행하는 프로세스는 foreground process로 지정되어 가장 높은 우선순위를 갖는다.
- 서비스가 start되었다면, 서비스를 실행하는 프로세스는 사용자에게 visible한 프로세스보다는 덜 중요하지만, 사용자에게 invisble한 프로세스보다는 중요하게 취급된다. 사용자에게 visible한 프로세스는 매우 소수이기 때문에 웬만해서는 종료되지 않지만, 어쨌든 종료될 가능성이 있긴 있으므로 주의해야 한다. 특히 서비스가 오랫동안 실행될수록 (작업을 수행하고 있지 않을 때) 일단 종료됐다가 나중에 다시 시작될 가능성이 높아진다.
- 서비스에 bound된 클라이언트가 있다면, bound된 프로세스보다 낮지 않은 우선순위를 갖는다. 따라서 bound된 프로세스가 사용자에게 visible하다면, 연결된 서비스도 visible한 것으로 취급된다.
- Start된 서비스에서 ``startForeground()`` API를 호출하면 서비스가 foreground 상태(사용자가 인식할 수 있는 상태)로 간주되어 정말 웬만하면 종료되지 않는다. 물론 메모리가 극한으로 부족한 상황에서는 종료될 수 있지만, 일반적으로는 종료되지 않는다고 봐도 된다.
메모리 확보를 위해 종료된 서비스는 나중에 여유가 생기면 다시 시작될 수 있다. 따라서 백그라운드나 다른 스레드에서 작업을 수행하는 스레드를 정의할 때에는, ``START_FLAG_REDELIVERY`` 플래그를 사용하여 서비스가 kill됐을 때의 Intent가 소실되지 않도록 하는 것이 좋다.