연결된 서비스

kosdjs·2025년 6월 23일
0

Android

목록 보기
10/24
  • 연결된 서비스는 클라이언트-서버 인터페이스의 서버에 해당함

  • 액티비티와 같은 컴포넌트가 서비스에 연결, 요청 전송, 응답 수신, IPC 수행을 가능하게 함

  • 일반적으로 백그라운드에서 무기한으로 실행되지 않으며, 다른 앱 컴포넌트에게 서비스를 제공하는 동안만 작동함

기본 사항

  • 연결된 서비스는 Service 클래스를 구현해 다른 앱이 연결되고 상호작용할 수 있게 함

  • 서비스 연결을 제공하려면 onBind() 콜백 메소드를 구현해야 함, 이 메소드는 서비스와 상호작용하기 위해 클라이언트가 사용할 수 있는 프로그래밍 인터페이스를 정의한 IBinder 객체를 반환함

시작된 서비스에 연결하기

  • 서비스 개요에서 설명했듯, 시작되고 동시에 연결되는 서비스를 만들 수 있음

  • startService()로 서비스를 시작해 무기한으로 실행되게 하고 동시에 bindService()로 클라이언트가 서비스와 연결하게 할 수 있음

  • 서비스가 시작되고 연결될 수 있게 만들었다면 시스템은 서비스가 시작되었을 때 모든 클라이언트와 연결 해제되기 전에는 서비스를 파괴하지 않음, 대신 반드시 서비스를 stopSelf()stopService()를 호출해 명시적으로 중단시켜야 함

  • 보통은 onBind()onStartCommand() 중 하나만 구현하면 되지만, 둘 다 구현해야할 상황이 가끔 있음, 예를 들어 음악 플레이어 앱의 경우 액티비티가 사용자가 앱을 떠나도 음악을 재생할 수 있게 음악 재생 서비스를 시작하고, 사용자가 앱을 떠났다가 다시 돌아왔을 때 액티비티가 음악 재생의 제어권을 되찾을 수 있게 서비스가 연결을 제공하는 경우에 해당함

  • 클라이언트가 bindService()를 호출할 때 서비스와 연결을 모니터링하는 ServiceConnection의 구현을 반환해야 함, 이 반환값은 요청하는 서비스가 존재하는지, 클라이언트가 해당 서비스에 액세스할 수 있는지 여부를 나타냄

  • 안드로이드 시스템이 클라이언트와 서비스의 연결을 생성할 때 ServiceConnectiononServiceConnected()를 호출함, 이 메소드는 클라이언트가 연결된 서비스와 통신할 때 사용할 수 있는 IBinder가 인수로 포함됨

  • 동시에 여러 클라이언트가 서비스에 연결될 수 있음, 이 때 시스템은 첫 클라이언트가 연결될 때만 onBind()메소드를 호출해 IBinder를 생성함, 다른 클라이언트가 연결될 때는 onBind()를 다시 호출하지 않고 첫 클라이언트가 연결될 때 생성된 IBinder를 전달함, 마지막 클라이언트가 연결 해제할 때 이 서비스가 시작된 서비스가 아니라면 시스템은 서비스를 파괴함

연결된 서비스 생성하기

  • 서비스가 연결을 제공한다면 반드시 클라이언트가 상호작용하기 위해 사용할 수 있는 인터페이스인 IBinder를 제공해야 함, 인터페이스를 정의하는 세가지 방식이 있음

Binder 클래스 확장

  • 서비스가 본인의 앱에서만 사용 가능하고 클라이언트와 같은 프로세스에서 실행된다면 Binder 클래스를 확장해 생성하고 onBind()에서 객체를 반환해 인터페이스를 만듬

  • 클라이언트가 Binder를 수신하면 서비스나 수신된 Binder 객체의 공개(Public) 메소드를 직접 사용할 수 있음

  • 서비스가 본인 앱의 백그라운드 작업만 하는 경우 선호되는 방법

Messenger 사용

  • 작업을 다른 프로세스 사이에서 해야 한다면 Messenger를 사용해 서비스를 위한 인터페이스를 생성할 수 있음, 이 경우 서비스는 다양한 타입의 Message 객체에 응답하기 위해 Handler를 정의해야 함

  • Handler는 클라이언트와 IBinder를 공유할 수 있는 Messenger의 기반이며, 클라이언트가 Message 객체를 사용해 서비스에 명령을 보낼 수 있도록 함

  • Messenger는 모든 요청을 싱글 스레드에서 처리하기 때문에 IPC를 수행하는 가장 간단한 방법

AIDL 사용

  • AIDL(Android Interface Definition Language)는 프로세스간 IPC를 수행하기 위해 객체를 운영체제 시스템이 이해할 수 있도록 기본형으로 분해하고 marshall

    참고: 마샬링(컴퓨터 과학)

  • Messenger를 사용하는 이전 방법은 사실 AIDL을 기본 구조로 사용함

  • 서비스가 여러 요청을 동시에 대응하게 만들 때 AIDL을 직접 사용할 수 있음, 이 때 서비스는 스레드로부터 안전하게 설계되어야 하고 멀티스레딩을 지원해야 함

  • AIDL을 직접 사용하려면 인터페이스를 정의하는 .aidl 파일을 생성해야 함, 안드로이드 SDK 도구는 인터페이스를 구현하고 IPC에 대응할 추상 클래스를 만드는데 이 파일을 사용함

노트: 대부분의 앱에서 AIDL은 멀티스레딩을 지원해야할 수 있고, 그 결과로 더 복잡한 구현이 되기 때문에 연결된 서비스를 만드는 최고의 방법이 아닙니다. 그러므로 이 문서는 어떻게 사용하는지 언급하지 않습니다. AIDL을 직접 사용하려면 AIDL 문서를 확인하세요.

Binder 클래스 확장

  • 서비스가 본인의 앱에서만 사용되고 프로세스 사이에서 작업할 필요가 없는 경우 클라이언트가 서비스의 공개 메소드에 직접 액세스할 수 있도록 Binder 클래스의 구현할 수 있음

노트: 이 방식은 클라이언트와 서비스가 같은 앱이고 같은 프로세스에 있을 경우에만 사용 가능합니다. 이는 매우 일반적인 경우입니다. 예를 들어, 음악을 백그라운드에서 재생하기 위해 서비스를 액티비티에 연결하는 음악 앱의 경우 잘 작동합니다.

설정하는 방법

  1. 다음과 같은 일중 하나를 하는 Binder 객체를 서비스에서 생성함
  • 클라이언트가 호출할 수 있는 공개 메소드를 포함하고 있는 객체
  • 클라이언트가 호출할 수 있는 공개 메소드를 가지고 있는 현재 서비스의 객체를 반환
  • 클라이언트가 호출할 수 있는 공개 메소드를 가지고 있는 서비스가 호스팅하는 다른 클래스의 객체를 반환
  1. onBind() 콜백 메소드에서 이 Binder 객체를 반환함

  2. 클라이언트는 onServiceConnected() 콜백 메소드에서 Binder를 수신해 사용 가능한 방법으로 연결된 서비스를 호출함

노트: 클라이언트가 제대로 반환된 객체를 캐스팅하고 그 객체의 API를 사용하려면 서비스와 클라이언트는 반드시 같은 앱에 있어야 합니다. 또한 이 방식은 프로세스간 marshalling을 수행하지 않기 때문에 서비스와 클라이언트가 반드시 같은 프로세스 안에 있어야 합니다.

Binder 구현체를 통해 클라이언트가 서비스의 메소드에 액세스할 수 있도록 하는 서비스 예시

class LocalService : Service() {
    // 클라이언트에게 제공되는 Binder 객체.
    private val binder = LocalBinder()

    // 난수 생성기.
    private val mGenerator = Random()

    /** 클라이언트가 사용할 메소드.  */
    val randomNumber: Int
        get() = mGenerator.nextInt(100)

    /**
     * 클라이언트 바인더에 사용되는 클래스. 이 서비스는 항상 클라이언트와
     * 같은 프로세스에서 실행되므로 IPC를 처리할 필요가 없습니다.
     */
    inner class LocalBinder : Binder() {
        // 이 LocalService 인스턴스를 반환해 클라이언트가 public 메서드를 호출할 수 있게 합니다.
        fun getService(): LocalService = this@LocalService
    }

    override fun onBind(intent: Intent): IBinder {
        return binder
    }
}
  • LocalBinder는 클라이언트에게 LocalService의 현재 객체를 전송하기 위해 getService() 메소드를 제공함, 이는 클라이언트가 getRandomNumber()와 같이 서비스의 메소드를 호출할 수 있게 함

LocalService와 연결되어 버튼을 누르면 getRandomNumber()를 호출하는 액티비티 예시

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) {
            // LocalService에 바인드되었으므로, IBinder를 형변환하여 LocalService 인스턴스를 가져옵니다.
            val binder = service as LocalService.LocalBinder
            mService = binder.getService()
            mBound = true
        }

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

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

    override fun onStart() {
        super.onStart()
        // LocalService에 바인드(연결)합니다.
        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) {
            // LocalService의 메서드를 호출합니다.
            // 그러나 이 호출이 오래 걸릴 수 있다면
            // 액티비티 성능 저하를 방지하기 위해 별도의 스레드에서 요청해야 합니다.
            val num: Int = mService.randomNumber
            Toast.makeText(this, "number: $num", Toast.LENGTH_SHORT).show()
        }
    }
}
  • 위 예제는 ServiceConnection의 구현제와 onServiceConnected() 콜백을 사용해 클라이언트가 서비스에 연결되는 법을 보여줌,

노트: 위 예제는 onStop() 메소드에서 클라이언트와 서비스의 연결을 해제합니다. 클라이언트와 서비스가 언제 연결 해제하는 것이 적절한지는 뒤의 추가 참고사항 섹션에서 언급합니다.

Messenger 사용

  • 서비스가 원격 프로세스와 통신해야 한다면, Messenger를 사용해 인터페이스를 제공할 수 있음

  • 이 방식은 AIDL을 사용하지 않고 IPC를 수행할 수 있는 방식임

  • Messenger는 서비스에 대한 모든 호출을 큐에서 처리하기 때문에 멀티스레딩을 반드시 대응해야하는 AIDL 인터페이스 방식보다 간단함

  • 대부분의 앱에서 서비스는 멀티스레딩을 처리할 필요가 없기 때문에 Messenger는 서비스가 호출을 한번에 하나씩 처리할 수 있게 만들어짐

Messenger 사용 요약

  1. 서비스에서 클라이언트의 호출에 대한 콜백을 받는 Handler를 구현

  2. 서비스는 Messenger 객체(Handler의 참조)를 생성하기 위해 Handler를 사용함

  3. 서비스가 onBind() 클라이언트에게 반환하는 IBinderMessenger가 생성함

  4. 클라이언트가 Message 객체를 서비스에 전달하기 위해 IBinder를 사용해 Messenger(서비스의 Handler를 참조)를 인스턴스화함

  5. handleMessage() 메소드를 통해 서비스는 Handler에서 각 Message를 전달받음

  • 이 방식을 사용하면 클라이언트가 서비스를 호출하기 위해 메소드를 사용하지 않음, 대신 클라이언트는 서비스에게 Handler를 통해 Message 객체를 전달함

Messenger 인터페이스를 사용하는 서비스 예시

/** 서비스에 메시지를 표시하라는 상수  */
private const val MSG_SAY_HELLO = 1

class MessengerService : Service() {

    /**
     * 클라이언트가 IncomingHandler에 메시지를 보낼 수 있도록 공개하는 대상
     */
    private lateinit var mMessenger: Messenger

    /**
     * 클라이언트로부터 들어오는 메시지를 처리하는 Handler
     */
    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 멤버의 값에 따라 무엇을 할 지 결정함

  • 클라이언트는 서비스가 반환하는 IBinder를 이용해 Messenger를 생성하고 send()를 사용해 메시지를 보내면 됨

서비스에 연결하고 MSG_SAY_HELLO 메시지를 보내는 액티비티 예시

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

    /** 서비스와 연결되었는지 여부를 나타내는 변수  */
    private var bound: Boolean = false

    /**
     * 서비스의 메인 인터페이스와 상호작용하는 클래스
     */
    private val mConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // 이 메소드는 서비스와 연결이 성립되었을 때 호출되며,
            // 서비스와 상호작용할 때 사용할 수 있는 객체를 제공합니다.
            // Messenger를 사용해 서비스와 통신하기 때문에
            // IBinder 객체를 받아 사용할 수 있습니다.
            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
        }
    }
}
  • 서비스에서 클라이언트에게 응답하게 만들고 싶다면 클라이언트에서도 Messenger를 만들면 됨, 클라이언트가 onServiceConnected() 콜백을 받았을 때 클라이언트의 Messengersend() 메소드의 매개변수 replyTo를 포함하는 Message를 보내면 됨, 예제는 MessengerService.javaMessengerServiceActivities.java를 확인

서비스에 연결

  • 앱 컴포넌트(클라이언트)가 서비스와 연결하기 위해 bindService()를 호출하면, 안드로이드 시스템은 서비스와 상호작용하기 위한 IBinder를 반환하는 서비스의 onBind()메소드를 호출함

  • 연결은 비동기식이고 bindService()는 클라이언트에 IBinder를 반환하지 않고 즉시 종료되기 때문에, IBinder를 받기 위해서는 클라이언트에서 ServiceConnection의 인스턴스를 생성하고 bindService()에 전달해야 함, ServiceConnection은 시스템이 IBinder를 전달할 수 있는 콜백 메소드를 포함하고 있음

노트: 오직 액티비티, 서비스, 컨텐츠 제공자만 서비스와 연결할 수 있습니다. 브로드캐스트 리시버에서 서비스와는 연결할 수 없습니다.

클라이언트에서 서비스와 연결하는 단계

  1. ServiceConnection 구현
  • 두 가지 메소드를 반드시 구현해야 함
    • onServiceConnected() : 서비스의 onBind() 메소드에서 반환되는 IBinder를 전달하기 위해 시스템이 호출하는 메소드
    • onServiceDisconnected() : 서비스가 비정상적으로 종료되거나 중단되는 것과 같이 서비스와의 연결이 예상치 못하게 끊어졌을 때 시스템이 호출하는 메소드, 서비스가 정상적으로 연결해제될 때는 호출되지 않음
  1. ServiceConnection의 구현체를 매개변수로 bindService()를 호출함

    노트: 메소드가 false를 반환했다면, 클라이언트는 서비스와 정상적으로 연결되지 않았음을 뜻합니다. 그럼에도 unbindService()를 호출해야 합니다. 그러지 않으면 서비스가 동작하지 않을 때 클라이언트가 서비스가 중단되는 것을 막을 수 있습니다.

  2. 시스템이 onServiceConnected() 콜백 메소드를 호출하고 나서 인터페이스로 정의된 메소드를 사용해 서비스를 호출할 수 있음

  3. unbindService()를 호출해 서비스와 연결을 해제함

  • 클라이언트와 서비스가 연결되어있을 때 클라이언트가 파괴된다면 서비스와 연결이 해제됨, 그래도 서비스와 상호작용이 끝났을 때 유휴 상태의 서비스가 중단될 수 있도록 연결을 해제하는 것이 더 좋음

Binder 클래스 확장에서 봤었던 서비스와 클라이언트가 연결하는 예제이므로, 반환된 IBinderLocalBinder로 형변환하고 LocalService 인스턴스를 요청하면 됨

var mService: LocalService

val mConnection = object : ServiceConnection {
    // 서비스와 연결이 성립되었을 때 호출됩니다.
    override fun onServiceConnected(className: ComponentName, service: IBinder) {
        // 같은 앱의 서비스를 명시적으로 연결하고 있기 때문에
        // IBinder를 구체적 클래스로 형변환해 사용할 수 있습니다.
        val binder = service as LocalService.LocalBinder
        mService = binder.getService()
        mBound = true
    }

    // 서비스와의 연결이 예상치 못하게 끊겼을 때 호출됩니다.
    override fun onServiceDisconnected(className: ComponentName) {
        Log.e(TAG, "onServiceDisconnected")
        mBound = false
    }
}

ServiceConnectionbindService()에 전달해 클라이언트와 서비스가 연결하는 예제

Intent(this, LocalService::class.java).also { intent ->
    bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
  • bindService()의 첫 매개변수는 연결하려는 서비스를 명시적으로 지정하는 Intent

  • 두번째 매개변수는 ServiceConnection 객체

  • 세번째 매개변수는 연결의 설정을 나타내는 변수임, 일반적으로 서비스를 생성하기 위해 BIND_AUTO_CREATE를 사용하고, 다른 값으로 BIND_DEBUG_UNBIND, BIND_NOT_FOREGROUND, 0 등 이 있음

추가 참고사항

  • 원격 메소드에서 발생하는 유일한 예외이고 연결이 끊어졌을 때 발생하는 예외인 DeadObjectException을 항상 처리해야 함

  • 객체는 프로세스 사이에서 참조 횟수가 계산됨

    참고: https://ko.wikipedia.org/wiki/%EC%B0%B8%EC%A1%B0_%ED%9A%9F%EC%88%98_%EA%B3%84%EC%82%B0_%EB%B0%A9%EC%8B%9D

  • 일반적으로 클라이언트의 생성, 파괴의 순간에 서비스에 연결, 연결 해제를 함

    • 액티비티가 보일 때만 서비스와 상호작용해야 한다면, onStart()에서 연결하고 onStop()에서 연결 해제를 함
    • 액티비티가 중단(stop) 되었을 때에도 응답을 받아야 한다면, onCreate()에서 연결하고 onDestroy()에서 연결 해제를 함, 이는 액티비티가 백그라운드에 있을 때에도 서비스를 사용하는 것이기 때문에 서비스가 다른 프로세스에 있다면 프로세스의 가중치가 늘어나 시스템에 의해 중단될 확률이 올라감

      노트: 서비스에 연결, 연결 해제하는 과정을 보통은 액티비티의 onResume(), onPause()에서 진행하지 않습니다. 이런 콜백들은 모든 수명주기 전환에서 일어나기 때문에 이런 전환에서의 처리를 최소화 히기 위해서입니다.

  • 만약 앱의 여러 액티비티가 같은 서비스에 연결하고 그 중 두 액티비티간의 전환이 있다고 했을 때, 연결과 연결 해제를 onResume(), onPause()에서 한다면 현재 액티비티에서 다음 액티비티로 전환하기 위해 pause 상태에서 연결을 해제하고 다음 액티비티가 resume 상태에서 연결을 한다면 서비스가 그 동안 파괴되고 다시 생성될 수 있음

연결된 서비스의 수명주기 관리

  • 서비스의 모든 연결이 해제되면 시스템은 서비스를 파괴함 (서비스가 startService()를 사용해 시작된 것이 아니라면), 그러므로 순수하게 연결만 되는 서비스라면 안드로이드 시스템이 서비스가 클라이언트와 연결되었는지를 확인해 관리하기 때문에 수명주기를 관리할 필요가 없음

  • onStartCommand()를 구현하기로 결정했다면, 서비스가 시작될 수 있기 때문에 반드시 서비스를 명시적으로 중단해야 함, 이런 경우 다른 클라이언트와 연결됨과 상관 없이 스스로 stopSelf()를 호출하거나 다른 컴포넌트가 stopService()를 호출하기 전까지 계속 실행됨

  • 서비스가 시작과 연결을 둘 다 허용한다면, 시스템이 onUnbind() 메소드를 호출할 때 다음에 클라이언트와 연결할 때 onRebind()에 대한 호출을 받고 싶다면 선택적으로 true를 반환할 수 있음, onRebind()는 반환값이 없지만 클라이언트는 onServiceConnected() 콜백에 있는 IBinder를 받음, 아래 그림은 이런 경우의 수명주기를 나타낸 그림

원문: https://developer.android.com/develop/background-work/services/bound-services

0개의 댓글