안드로이드, 어디까지 아세요 [2.1] — background task, Service, Foreground Service

안드로이드의 백그라운드 작업 처리 종류와 Service의 이해및 구현

MJ Studio
MJ Studio

--

안드로이드, 어디까지 아세요 시리즈의 두 번째 글입니다. 이 포스팅은 안드로이드의 4대 컴포넌트 중 하나인 Service에 대한 이해를 목적으로 작성되었으며 Service에 대해 알아 볼것이 많기 때문에 2라는 대단원 안에 쪼개서 포스팅을 작성할 것입니다.

이 포스팅에서는 Android의 Background task를 처리하는 데 사용될 수 있는 방법들에 대해 알아보고 Service의 필요성과 API를 살펴 본 후에 Service, Foreground Service를 직접 구현해보겠습니다.

다음은 샘플 저장소입니다.

Android Background task

우리가 안드로이드 개발을 하며 어떠한 작업을 백그라운드에서 진행해주거나 비동기적으로 실행시켜주고 싶을 때 무엇을 사용해야 할까요? 안드로이드 도큐먼트에는 이러한 선택의 경우들에 알맞는 선택지를 친절하게 제공해줍니다.

우선 Background task들을 어떻게 분류할 수 있는지를 살펴보겠습니다.

https://developer.android.com/guide/background

할 일(Task)는 즉시 앱을 사용하는 유저에게 응답을 주어야 한다면 Immediate 로, 그렇지 않고 미래의 특정 시점에 언제든 완료되도 좋다면 Exact(정확한 시간에 실행되어야 함)이나 Deferred(완료되는 시점이 중요치 않음)으로 분류됩니다.

이러한 할 일의 분류에 대한 적절한 해결책은 다음과 같습니다.

Immediate

우선, 앱이 Foreground 상태에 있을 때 완료될 수 있는 작업(간단한 API 호출, 짧은 시간의 타이머, 금방 끝나는 계산집약적 알고리즘) 등은 쓰레드를 사용해서 손쉽게 해결될 수 있습니다. 쓰레드를 쓰는 테크닉은 Java의 Thread를 Executor와 함께 사용하거나, Thread를 손쉽게 쓰게 만들어주는 라이브러리인 RxJava나 코틀린의 Coroutine을 사용할 수 있습니다.

만약, 우리의 작업이 즉각적으로 앱을 사용하는 유저에게 응답을 주어야 하지만, 앱이 백그라운드 상태에 있거나 기기가 재시작되어도 지속적으로 진행이 되어야하는 작업이라면 WorkManager를 사용할 수 있습니다. WorkManager에 대해서는 이전에 작성한 포스팅이 있으니 참고하시면 좋겠습니다.

음악을 재생하는 앱을 만들거나 달리기 앱을 만들어 현재 상황을 유저에게 지속적으로 보여줘야 하는 경우엔 Foreground Service를 사용할 수 있습니다. 이번 포스팅에 집중적으로 알아볼 Service의 한가지 종류입니다.

Deferred

역시나 WorkManager입니다. WorkManager는 디바이스의 안드로이드 API 버전에 따라 자동적으로 여러 구현체를 이용해 동작되므로 간단한 문법과 좋은 성능을 기대할 수 있습니다.

Exact

알람앱같이 정확한 시간에 유저에게 메세지를 보내주어야 한다면 우리는 AlarmManager를 사용할 수 있습니다. 이와 관련해서도 이전에 포스팅을 쓴 적이 있으니 가볍게 살펴보시면 좋을 것 같습니다.

Service 의 필요성

Service는 Android의 4대 컴포넌트(Activity, ContentProvider, BroadcastReceiver, Service)중 하나입니다. 도큐먼트에는 Service의 주된 목적이 오래 걸리는 작업을 백그라운드에서 처리하는 것이라고 적혀있습니다. 그러나 앞서 살펴보았듯이 modern한 안드로이드 개발에서 background tasks를 수행하는 데 서비스를 써야할 일은 그리 많지 않습니다. 그러면 왜 서비스를 공부하고 알아야할까요?

  1. Foreground Service는 필요하다.
  2. WorkManager같은 라이브러리들은 내부적으로 Service를 사용할 수도 있다.
  3. Service는 AIDL(Android Interface Definition Layer)를 이용한 Java, C++ 혹은 HAL(Hardware Abstraction Layer)과의 IPC(Inter-Process Communication)에 쓰인다.

이 포스팅은 1번과 3번에 대한 학습을 목적으로 방향성을 잡았습니다. 서비스의 개념과 문법, 그리고 1,3을 구현하는 코드도 한번 살펴보는 시간을 갖겠습니다. 3번에 나오는 용어인 AIDL에 대한 설명은 우선 넘어가겠습니다.

Service 의 종류

안드로이드 도큐먼트에서 정의하는 서비스의 종류는 크게 세 가지가 있습니다.

  1. Foreground
  2. Background
  3. Bound

이 중에서 1, 2는 Started Service라 불리고 3은 Bound Service라 불립니다.

Foreground는 미리 살펴보았듯이 안드로이드의 Notification을 사용하여 다운로드가 되는 상황을 알림에 progress와 함께 보여주거나 음악앱의 현재 플레이백 상태를 알림과 함께 업데이트 해주곤 합니다.

Background는 백그라운드에서 실행되지만 유저에게 특별히 진행상황이나 결과에 대한 메세지를 전달할 필요가 없는 경우에 쓰입니다. API 26부터는 백그라운드 서비스에서 여러가지 제약이 걸렸습니다. 이는 백그라운드에서 돌고있는 앱이 시스템의 리소스를 많이 잡아먹으면 유저가 다른 앱을 사용하고 있을 때도 좋지못한 사용자 경험을 제공할 수 있기 때문입니다. 그렇기 때문에 우리는 Background Service를 사용할 수 있는(굳이 사용할 필요가 없어서) 영역이 더욱 줄어들었습니다.

Bound는 서비스가 bind된 상태입니다. Service-Client 모델에서 서비스가 특별한 클라이언트에게 결합된 상태를 의미하는데, 클라이언트에게 서비스가 기능을 제공할 수 있도록 초기화가 되었다고 이해할 수 있겠습니다. 여기서 중요한건 Service와 bind될 수 있는 클라이언트의 종류가 안드로이드의 컴포넌트들이나 IPC를 하는 다른 프로세스일 수도 있다는 것입니다.

세 가지 종류의 서비스가 있지만, 이는 모두 독립된 것은 아닙니다. 서비스 하나가 구현 이 어떻게 되냐에 따라 binding(Bound) 되는 용도로도 쓰이며 동시에 Foreground, Background(Started)로도 쓰일 수 있습니다.

https://developer.android.com/guide/components/services

Service 구현

Service에도 안드로이드의 여타 컴포넌트들 처럼 생명주기 메소드들이 있습니다.

  • onCreate(): 서비스 객체개 새롭게 생성이 될 때 호출됩니다. onStartCommand, onBind 보다 제일 먼저 호출됩니다.
  • onDestroy(): 서비스 객체가 파괴될 때 호출됩니다.
  • onStartCommand(): 시스템이 startService 함수를 이용해 서비스를 시작할 때 호출됩니다. 서비스를 시작하면 서비스를 중단하는 것은 우리의 몫입니다. 서비스 내에서 직접 stopSelf를 호출하거나 밖에서 stopService를 호출해주면 됩니다. 이는 새롭게 startCommand 호출이 일어날 때 여러번 호출될 수 있습니다.
  • onBind(): 새로운 binding이 연결될 때 호출됩니다. 바인딩에 대해서 설명할 때 더 자세히 알아볼 것이며 IBinder의 구현체나 null을 반환해야 합니다. 새로운 바인딩이 결합될 때마다 여러번 호출될 수 있습니다.

Example: Basic started Service

실제로 예제를 만들어보겠습니다.

Service도 안드로이드의 컴포넌트이기 때문에 AndroidManifest.xml <service>에 꼭 명시해주어야 한다는 점을 유의하시기 바랍니다. 여기엔 다른 앱이 이 앱의 서비스를 사용할 수 있게 할지를 결정하는 exported 어트리뷰트를 포함한 다른 여러가지 어트리뷰트를 설정할 수 있습니다. 특히 도큐먼트에서는 intent-filter 를 사용하여 다른 시스템이 우리의 서비스를 암시적으로 실행시킬 수 있게 만드는 것을 보안적인 이슈를 방지하기위해 금지하라고 경고합니다.

startServicestopService메소드를 이용해서 서비스를 시작하고 중지할 수 있습니다.

onCreateonDestroy에서 로그가 찍히게 하였고, onStartCommand에서 현재 어떤 쓰레드에서 동작하고 있는 지 로그가 찍히게 했습니다. 실제 결과를 보시죠.

볼 것은 두가지입니다. onCreate와 onDestroy는 서비스가 시작되고 파괴될때 잘 찍힙니다.

  1. 서비스가 생성된 후에도 계속 서비스를 시작하여 onStartCommand를 호출할 수 있다.
  2. 서비스가 실행되는 쓰레드는 메인쓰레드이다.

서비스는 기본적으로 상주하는 프로세스의 메인쓰레드에서 동작합니다. 그렇기 때문에 ANR(Application Not Responding)을 피하고 싶다면 따로 쓰레드를 파서 원하는 작업을 해주어야 합니다. 이를 자동으로 쓰레드를 만들고 실행시켜주게 설계된 것이 IntentService인데, 이는 현재 앞서 언급한 API 26 부터의 background Service limitation에 의해 deprecated 되었고 내부적으로 Service가 아닌 JobScheduler를 사용하는 JobIntentService 를 사용하기를 도큐먼트에서는 권장하고 있습니다.

onStartCommand의 반환값

서비스는 언제든지 시스템에 의해 자원이 부족한 상황에 종료될 수 있습니다. 우리는 이러한 상황을 대비하기 위해 우아한(gracefully) 재시작 루틴을 설계해야 합니다.

이러한 루틴을 설계할 때 선택할 수 있는 옵션들은 onStartCommand의 반환값을 통해 결정됩니다. 어떤 것을 반환하느냐에 따라 재시작될 때 onStartCommand가 호출되는 방식이 달라집니다.

  • START_NOT_STICKY: 서비스를 명시적으로 다시 시작할 때 까지 만들지 않습니다.
  • START_STICKY: 서비스를 다시 만들지만 마지막 IntentonStartCommand의 인자로 다시 전달하지 않습니다. 이는 일단 계속 살아있어야되지만 별다른 동작이 필요하지 않은 음악앱같은 서비스에 적절합니다.
  • START_REDELIVER_INTENT: 이름에서 알겠듯이 마지막 IntentonStartCommand의 인자로 다시 전달해줍니다. 즉각적인 반응이 필요한 파일 다운로드 서비스 같은 곳에 적합합니다.

Foreground Service

https://developer.android.com/guide/components/foreground-services

Foreground Service는 현재 서비스의 동작이나 상황을 유저에게 계속 알려주어야 하는 상황에서 유용합니다. 마치 음악 앱이나 달리기 기록을 측정하고 있는 운동 앱이 그 예가 될 수 있습니다. Foreground Service는 현재 실행되고 있다는 것을 시스템의 status bar에 표시를 해야하기 때문에 꼭 Notification 과 함께 사용되어야 합니다.

API 28부터는 Foreground Service를 사용하기 위해 권한을 요청해야 합니다. 이는 일반 수준의 권한이기 때문에 명시만 해놓으면 자동으로 시스템이 허가를 해줍니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>

<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

<application ...>
...
</application>
</manifest>

Example: Basic started Service

간단하게 NotificationCompat.BuildersetProgress 를 이용해서 비디오를 다운로드하는 상황을 위한 foreground Service 를 만들어보겠습니다.

이에 관련해서 쓰레드 처리나 중지 처리는 제대로 처리가 안되어있습니다. 당연히 thread 빌더만으로 Service 내에서 비동기를 처리하는 것은 많은 이슈가 발생할 것입니다.

Foreground Service를 실행시켜 줄땐 API 26보다 위라면 startService 대신 startForegroundService 를 호출해주면 앱이 foreground 상태가 아닐때도 서비스를 실행할 수 있습니다.

Notification 관련해서 코드가 길어졌지만, 별 것없습니다. 저희가 유의할 점은 onStartCommand의 제일 첫줄에 startForeground 메소드를 호출해서 Notification의 id와 Notification 객체를 전달해주는 것입니다. 전달된 Notification은 Foreground Service가 실행되는 동안 status bar에 상주하게 되고 지울 수 없습니다.

이를 끝내려면 stopForeground(boolean)을 호출할 수 있는데, 인자 boolean은 현재 존재하는 Notification을 status bar에서 없앨 것인지를 결정합니다. stopForeground를 호출해도 서비스는 종료되지 않고 background 서비스로 바뀌는 것임을 명심하시길 바랍니다.

결론

안드로이드 컴포넌트 중 하나인 서비스에 대해 개념과 간단한 구현을 알아보는 시간을 가졌습니다. 다음 포스팅에서는 Service를 이용한 여러 방법의 IPC 방법과 AIDL 에 대해서 정리해보겠습니다.

감사합니다 ⭐️

--

--