본문 바로가기

안드로이드

[안드로이드] 서비스(Service)

서비스(Service)

안드로이드의 서비스란 긴 작업을 백그라운드에서 수행하는데 사용되는 UI를 제공하지 않는 컴포넌트이다. 그리고 IPC(Inter-Process Communication; 프로세스간 통신)를 지원한다. 서비스가 사용되는 예시로는 음악 재생, 네트워킹, 파일 입출력, Content Provider와 상호작용 등이 있다. 특별히 중요한 점은 백그라운드에서 수행한다는 말이 메인 쓰레드에서 수행되지 않는 다는 말과 같지 않다는 점이다. 즉, 서비스는 메인 쓰레드에서 수행된다. 그렇기에 음악재생, 네트워킹과 같은 Intensive I/O 작업들은 서비스 내에서 별도의 쓰레드를 생성하여 작업하는 것이 올바르다.

서비스의 생명주기

 


서비스의 세 가지 유형

1. 포그라운드 서비스

포그라운드 서비스는 대표적인 예시로 음악 재생이 존재한다. 앱 UI가 메인 화면에는 보이지 않지만 notification의 형태로 존재한다. 유저 입장에서는 백그라운드처럼 느껴지지만 개발자 입장에서는 포그라운드 서비스이다.  포그라운드 서비스라는 이름이 붙은 이유는 어플리케이션이 계속 실행중이기 때문이다. 안드로이드의 작업 관리자에서 앱을 종료하면 음악 재생이 종료되는 것을 생각하면 쉽다.

 

2. 백그라운드 서비스

백그라운드 서비스는 사용자는 전혀 볼 수 없는 서비스이다. notification이 존재하지 않고, 앱이 종료되어도 백그라운드 서비스는 계속해서 수행중이다. 다만 너무 많은 서비스가 유저 기기에 존재하면 성능 저하라는 문제점이 생긴다. 서비스는 결국 RAM에 상주하기 때문이다. 안드로이드에서는 이러한 점을 문제로 삼아 API가 26(Android 8.0)이상인 버젼에서는 백그라운드 서비스 실행에 대한 제한을 적용한다.

 

이러한 백그라운드 서비스 실행 제한의 문제는 안드로이드 Jetpack의 WorkManager로 작업을 예약하는 방식으로 해결 가능하다. 

 

백그라운드 서비스 실행 제한
1. Service: 앱이 idle한 상태일 경우 백그라운드 서비스 실행에 제한이 걸린다. 
2. Broadcast: Manifest-registerd 브로드캐스트 리시버에서 암시적 브로드캐스트는 불가능하다. Manifest에 등록된 브로드캐스트 리시버의 경우 모두 명시적으로 action을 정의해주어야 한다.
<receiver
    android:name=".broadcastreceiver.BatteryReceiver"
    android:enabled="true"
    android:exported="true">
    <!--<intent-filter>//오래오 이전버전에서는 이렇게 해야됨-->
            <!--<action android:name="android.intent.action.ACTION_POWER_CONNECTED"/>-->
    <!--</intent-filter>-->
</receiver>

 

3. 바운드 서비스

특정 컴포넌트에서 bindService()를 호출하여 서비르를 바인딩할 수 있다. 이러한 바운드 서비스의 경우 컴포넌트와 client-server 구조의 방식으로 요청을 보내고 결과를 받거나 IPC가 가능하다. 여러 컴포넌트가 하나의 바운드 서비스를 바인딩할 수 있고 모든 컴포넌트에서 바인딩을 해제하면 바운드 서비스는 소멸된다.

 


서비스 구현 방법

1. Manifest에 등록

먼저 Service를 부모로 두는 하위 클래스 중 하나를 선택하여 상속한 클래스를 구현하여야한다. 그리고 이 클래스를 AndroidManifest.xml에 등록한다.

 

추가로 서비스를 시작할 때는 명시적 인텐트를 사용할 것을 권장한다. 암시적 인텐트로 서비스를 시작하게 된다면 보안 위험을 초래하기 때문이다. android:exported를 false로 설정하면 서비스를 본인의 앱에서만 사용하여 다른 앱은 사용할 수 없도록 설정할 수 있다.

 

추가로 유저는 어떤 서비스가 실행 중인지 기기에서 확인할 수 있고 심지어 종료시킬 수도 있기 때문에 android:description을 정의하여 이 서비스가 하는 일을 명시해주어야 유저가 서비스를 종료하는 불상사를 막을 수 있다.

<manifest ... >
  ...
  <application ... >
      <service android:name=".ExampleService" 
      android:description="자료를 다운로드하는 서비스입니다."/>
      ...
  </application>
</manifest>

 

2. 서비스 구현

위에서 말한듯 먼저 Service를 부모로 두는 하위 클래스 중 하나를 선택하여 상속한 클래스를 구현하여야한다. 대표적으로 가장 기초적인 Service와 IntentService가 존재한다. 본 문서에서는 IntentService를 상속하여 구현한다. IntentService는 work thread에서 요청된 작업을 한번에 하나씩 순서대로 처리한다. 별도의 쓰레드 생성과 관련한 코딩이 없어져 구현이 쉽다. 동시에 여러개의 작업을 처리할 필요가 없을 경우에는 가장 좋은 선택이다. 그리고 Service에 비해 구현할 메서드의 수도 훨씩 적어 코딩이 편해진다.

 

class HelloIntentService : IntentService("HelloIntentService") {

    override fun onHandleIntent(intent: Intent?) {
        // 작업할 내용을 작성한다.
        // 아래 예제는 5초간 sleep 상태에 들어가는 예제이다.
        try {
            Thread.sleep(5000)
        } catch (e: InterruptedException) {
            // Restore interrupt status.
            Thread.currentThread().interrupt()
        }

    }
}

(참고로 IntentService는 Android 11(API 30)부터 Deprecated 될 예정이다. JobIntentService 사용을 추천)

 

 

3. 서비스 시작

액티비티나 다른 컴포넌트에서 서비스를 실행하려면 startService 혹은 startForegroundService를 실행하면 된다.

 

백그라운드 서비스

Intent(this, HelloService::class.java).also { intent ->
    startService(intent)
    //startForegroundService(intent)
}

 

포그라운드 서비스

그리고 특히 포그라운스 서비스의 경우 다음과 같이 startForground() 메서드를 수행해야한다. startForegroundService() 이후 startForeground()를 5초 이내에 수행하지 않을 경우 크래쉬가 난다.  startForegroundService()는 첫번째 매개변수로 알림을 고유하게 식별하는 변수, 두번째로 상태표시줄에 보일 notification을 전달해준다.

val pendingIntent: PendingIntent =
        Intent(this, ExampleActivity::class.java).let { notificationIntent ->
            PendingIntent.getActivity(this, 0, notificationIntent, 0)
        }

val notification: Notification = Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE)
        .setContentTitle(getText(R.string.notification_title))
        .setContentText(getText(R.string.notification_message))
        .setSmallIcon(R.drawable.icon)
        .setContentIntent(pendingIntent)
        .setTicker(getText(R.string.ticker_text))
        .build()

startForeground(ONGOING_NOTIFICATION_ID, notification)