본문 바로가기
Android/Components

[안드로이드] App Components - Service

by SungJe 2021. 5. 27.

Service란?

안드로이드 개발자 사이트에서 Service는 다음과 같이 설명하고 있다.

A service is a general-purpose entry point for keeping an app running in the background for all kinds of reasons. It is a component that runs in the background to perform long-running operations or to perform work for remote processes.
Link: Google Developer

즉, Service는 백그라운드에서 앱을 계속 실행하기 위한 진입점으로 오랫동안 실행되는 작업이나 원격 프로세스를 위한 작업을 수행한다.

서비스의 세 가지 유형

  • 포그라운드(Forground): 사용자에게 잘 보이는 몇몇 작업을 수행한다. 포그라운드 서비스는 알림을 표시하며 사용자가 앱과 상호작용하지 않을 때도 계속 실행된다. (e.g. 오디오 앱의 트랙 재생)

  • 백그라운드(Background): 사용자에게 직접 보이지 않는 작업을 수핸한다. (e.g. 저장소 압축)

  • 바인드(Bound): 클라이언트-서버 인터페이스를 제공하여 구성 요소가 서비스와 상호작용하며 결과를 받을 수 있다.

Service Lifecycle

  • onCreate(): 서비스가 처음 생성되었을 때 호출하는 콜백 메서드이다. 일회성 설정 절차를 수행되며 만약 서비스가 이미 실행 중인 경우 이 메서드는 호출되지 않는다.

  • onStartCommand(): 다른 컴포넌트가 서비스를 시작하도록 요청하는 경우 호출하는 콜백 메서드이다. 서비스가 시작되면 백그라운드에서 무한히 실행될 수 있기 때문에 중단을 위해 stopSelf() 또는 stopService()를 호출하여 중단할 수 있다.

  • onBind(): 다른 컴포넌트가 서비스에 바인딩되고자 하는경우 호출하는 콜백 메서드이다. 메서드를 구현할 때 클라이언트가 서비스와 통신을 주고받기 위한 IBinder 인터페이스를 제공해야 한다.

  • onDestroy(): 더 이상 서비스를 사용하지 않고 소멸시킬 때 호출하는 콜백 메서드이다. 서비스가 수신하는 마지막 호출로 스레드와 리스너 등과 같은 리소스를 정리하기 위한 코드를 구현해야한다.

Service Lifecycle

Manifest 환경 설정

Service도 다른 컴포넌트와 마찬가지로 애플리케이션의 매니페스트 파일에서 선언해야 한다.

Service 선언

<service>의 유일한 필수 요소는 android:name이다. 그 외의 속성에 관한 자세한 내용은 개발자 가이드에서 확인 할 수 있다.

<manifest ... >
  ...
  <application ... >
    <service android:name=".ExampleService" />
    ...
  </application>
  ...
</manifest>

Service 생성

service는 다른 컴포넌트가 startService()를 호출하여 생성되고, 그 결과로 서비스의 onStartCommand() 메서드가 호출된다.
서비스가 시작되면 이를 생성한 컴포넌트와 독립적인 수명 주기를 가지게 된다. 서비스가 백그라운드에서 무한히 실행될 수 있으며, 서비스를 생성한 컴포넌트가 소멸되었더라도 무관하기 때문에 작업이 완료되면 stopSelf()나 stopService() 메서드를 호출하여 중단시킬 수 있다.
다음은 service를 생성하기 위한 몇 가지 방법에 대해 알아본다.

IntentService 클래스 확장

IntentService는 백그라운드 스레드로 동작하기 때문에 별도의 스레드 처리가 필요 없다. 작업은 순차적으로 처리되며 한 번에 하나의 작업만 수행된다. API 30에서 deprecated가 되어 JobIntentService로 대체되었다.

class HelloIntentService : IntentService("HelloIntentService") {
    override fun onHandleIntent(intent: Intent?) {
        // 단순히 5초간 sleep
        try {
            Thread.sleep(5000)
        } catch (e: InterruptedException) {
            // 인터럽트 상태를 복원함
            Thread.currentThread().interrupt()
        }
    }
}

Service 클래스 확장

서비스가 멀티스레딩을 수행해야 하는 경우 Service 클래스를 확장하여 각 인텐트를 처리할 수 있다. 기본적으로 UI 스레드이며, 별도의 스레드 처리가 필요하다.
다음은 IntentService 클래스의 샘플과 같은 작업을 수행하는 Service 클래스의 구현이다.

class HelloService : Service() {

    private var serviceLooper: Looper? = null
    private var serviceHandler: ServiceHandler? = null

    // 스레드에서 메시지를 수신하는 핸들러
    private inner class ServiceHandler(looper: Looper) : Handler(looper) {

        override fun handleMessage(msg: Message) {
            // 단순히 5초간 sleep
            try {
                Thread.sleep(5000)
            } catch (e: InterruptedException) {
                // 인터럽트 상태를 복원함
                Thread.currentThread().interrupt()
            }

            // Start ID를 이용하여 서비스 중지
            stopSelf(msg.arg1)
        }
    }

    override fun onCreate() {
        // 서비스를 실행하는 스레드를 시작한다.
        HandlerThread("ServiceStartArguments", Process.THREAD_PRIORITY_BACKGROUND).apply {
            start()

            serviceLooper = looper
            serviceHandler = ServiceHandler(looper)
        }
    }

    override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
        Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show()

        // 각 시작 요청에 대해 메시지를 보내 작업을 시작하고
        // Start ID를 전달하여 작업을 마치면 중지되는 요청을 알 수 있다.
        serviceHandler?.obtainMessage()?.also { msg ->
            msg.arg1 = startId
            serviceHandler?.sendMessage(msg)
        }

        return START_STICKY
    }

    override fun onBind(intent: Intent): IBinder? {
        // 바인딩을 제공하지 않으므로 null을 반환한다.
        return null
    }

    override fun onDestroy() {
        Toast.makeText(this, "service done", Toast.LENGTH_SHORT).show()
    }
}

보시다시피 IntentService를 사용할 때보다 훨씬 손이 많이 간다.

Bound Service 생성

바인딩을 제공하는 서비스를 생성할 때는 클라이언트가 서비스와 상호작용하기 위한 IBinder 인터페이스를 제공해야 한다. 인터페이스를 정의하는 방법은 세 가지가 있다.

1. 바인더 클래스 확장

서비스가 로컬 애플리케이션에서만 사용되고 여러 프로세스에서 작동할 필요가 없는 경우, 자체적인 Binder 클래스를 구현하여 클라이언트가 서비스 내의 공개 메서드에 직접 액세스하도록 할 수 있다.

class LocalService : Service() {
    private val binder = LocalBinder()
    private val mGenerator = Random()
    val randomNumber: Int
        get() = mGenerator.nextInt(100)

    // 클라이언트 바인더에 사용되는 클래스
    inner class LocalBinder : Binder() {
        fun getService(): LocalService = this@LocalService
    }

    override fun onBind(intent: Intent): IBinder {
        return binder
    }
}

LocalBinder는 LocalService의 현재 인스턴스를 검색하기 위한 getService() 메서드를 클라이언트에 제공한다. 이를 통해 클라이언트가 서비스 내의 public 메서드를 호출할 수 있다.

다음은 버튼을 클릭했을 때 LocalService에 바인딩되어 getRandomNumber()를 호출하는 Acvivity이다.

class BindingActivity : Activity() {
    private lateinit var mService: LocalService
    private var mBound: Boolean = false

    // bindService()에 전달될 서비스 바인딩에 대한 콜백을 정의
    private val connection = object : ServiceConnection {
        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            val binder = service as LocalService.LocalBinder
            mService = binder.getService()
            mBound = true
        }

        override fun onServiceDisconnected(arg0: ComponentName) {
            mBound = false
        }
    }

    override fun onStart() {
        super.onStart()
        // 로컬 서비스에 바인딩
        Intent(this, LocalService::class.java).also { intent ->
            bindService(intent, connection, Context.BIND_AUTO_CREATE)
        }
    }

    override fun onStop() {
        super.onStop()
        unbindService(connection)
        mBound = false
    }

    // 버튼 클릭 이벤트(레이아웃 파일의 android:onClick 속성에 적용)
    fun onButtonClick(v: View) {
        if (mBound) {
            // 로컬 서비스에서 메서드를 호출
            // 만약 호출이 중단될 수 있는 경우, 별도의 스레드에서 수행되어야 한다.
            val num: Int = mService.randomNumber
            Toast.makeText(this, "number: $num", Toast.LENGTH_SHORT).show()
        }
    }
}

2. 메신저 사용

서비스가 원격 프로세스와 통신해야 한다면 Messenger를 사용하여 서비스에 인터페이스를 제공할 수 있다. 이 기법을 사용하여 AIDL을 쓰지 않고도 프로세스 간 통신(IPC)을 실행할 수 있다.

/** 서비스에 대한 명령  */
private const val MSG_SAY_HELLO = 1

class MessengerService : Service() {

    // 대상 클라이언트가 IncomingHandler로 메시지를 보낼 수 있도록 게시
    private lateinit var mMessenger: Messenger

    // 클라이언트에서 수신하는 메시지 핸들러
    internal class IncomingHandler(
            context: Context,
            private val applicationContext: Context = context.applicationContext
    ) : Handler() {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                MSG_SAY_HELLO ->
                    Toast.makeText(applicationContext, "hello!", Toast.LENGTH_SHORT).show()
                else -> super.handleMessage(msg)
            }
        }
    }

    // 서비스에 바인딩할 때, 서비스에 메시지를 보내기 위한 인터페이스를 반환
    override fun onBind(intent: Intent): IBinder? {
        Toast.makeText(applicationContext, "binding", Toast.LENGTH_SHORT).show()
        mMessenger = Messenger(IncomingHandler(this))
        return mMessenger.binder
    }
}

Handler의 handleMessage() 메서드에서 서비스가 수신되는 Message를 받고 what 멤버에 기초하여 무엇을 할지 결정한다.

다음은 서비스에 바인딩되어 MSG_SAY_HELLO 메시지를 서비스에 전달하는 Activity이다.

class ActivityMessenger : Activity() {
    // 서비스와 통신하기 위한 메신저
    private var mService: Messenger? = null

    // 서비스에서 바인드를 호출했는지 여부를 나타내는 플래그
    private var bound: Boolean = false

    // 서비스의 기본 인터페이스와 상호 작용하기 위한 클래스
    private val mConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // 서비스와 연결되었을 때 호출되어 상호 작용하기 위한 개체를 제공한다.
            // Messenger로 서비스와 소통하며, IBinder는 클라이언트로 부터 representation을 얻는다.
            mService = Messenger(service)
            bound = true
        }

        override fun onServiceDisconnected(className: ComponentName) {
            // 서비스 프로세스가 중단되었을 때 호출된다.
            mService = null
            bound = false
        }
    }

    fun sayHello(v: View) {
        if (!bound) return

        // 'what' 값을 사용하여 서비스 메시지 생성 및 전송
        val msg: Message = Message.obtain(null, MSG_SAY_HELLO, 0, 0)
        try {
            mService?.send(msg)
        } catch (e: RemoteException) {
            e.printStackTrace()
        }
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.main)
    }

    override fun onStart() {
        super.onStart()
        // 서비스에 바인딩
        Intent(this, MessengerService::class.java).also { intent ->
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
        }
    }

    override fun onStop() {
        super.onStop()
        // 서비스에 바인딩 해제
        if (bound) {
            unbindService(mConnection)
            bound = false
        }
    }
}

3. AIDL(Android Interface Definition Language) 사용

객체를 운영체제가 이해할 수 있는 원시 유형으로 해체한 다음, 여러 프로세스를 집결시켜 프로레스 간 통신(IPC)을 실행한다.

Note: 대부분의 애플리케이션은 바인드 서비스를 생성할 때 AIDL을 사용하면 안된다. 이 방법은 다중 스레딩 기능이 필요할 수 있고 구현이 더욱 복잡해 지기 때문에 대부분의 애플리케이션에 적합하지 않다. AIDL을 반드시 직접 사용해야 할 경우에는 AIDL 문서를 참고한다.

서비스 바인딩

bindService()를 호출하면 애플리케이션 컴포넌트(클라이언트)를 서비스에 바인딩할 수 있다. Android 시스템이 onBind() 메서드를 호출하고, 이 메서드가 서비스와 상호작용을 위한 IBinder를 반환한다.

Note: Activitys, Services, Content Providers은 서비스 바인딩할 수 있으나 Broadcast Receiver에는 서비스 바인딩할 수 없다.

클라이언트에서 서비스 바인딩하려면 다음 단계를 따른다.

1. ServiceConnection 구현
두 가지 콜백 메서드를 재정의해야 한다.

  • onServiceConnected(): 시스템이 이를 호출하여 서비스의 onBind() 메서드가 반환한 IBinder를 전달한다.
  • onServiceDisconnected(): 서비스가 비정상 종료나 중단되었을 때 이를 호출한다. 클라이언트에 의해 바인딩을 해제할 때는 호출되지 않는다.

2. bindService() 를 호출하여 ServiceConnection 구현을 전달

서비스 바인딩 예시

var mService: LocalService

// ServiceConnection 구현
val mConnection = object : ServiceConnection {
    override fun onServiceConnected(className: ComponentName, service: IBinder) {
        val binder = service as LocalService.LocalBinder
        mService = binder.getService()
        mBound = true
    }

    override fun onServiceDisconnected(className: ComponentName) {
        Log.e(TAG, "onServiceDisconnected")
        mBound = false
    }
}

// bindService() 메서드 호출
Intent(this, LocalService::class.java).also { intent ->
    bindService(intent, connection, Context.BIND_AUTO_CREATE)
}

참고 사이트