Android - AIDL을 이용하여 Remote Service 구현

JS · 21 Aug 2020

Android에서 Remote Service를 구현하는 방법은 AIDL(Android Interface Definition Language) 또는 Messenger를 이용하는 방법이 있습니다. Messenger는 내부적으로 AIDL을 이용하여 IPC(Inter-process communication)를 수행합니다.

AIDL과 Messenger의 차이점은 다음과 같습니다.

  • AIDL : Android는 Binder를 이용하여 IPC를 수행합니다. AIDL은 인터페이스 정의로, 정의된 인터페이스에 필요한 Binder 코드들을 컴파일 과정에서 자동 생성합니다. AIDL로 Remote service를 구현하면 멀티쓰레드에서 여러 요청을 동시에 요청하거나 처리할 수 있습니다. 물론 쓰레드에 안전하도록 구현을 해야 합니다.
  • Messenger : 내부적으로 AIDL을 이용하여 구현되어있습니다. AIDL을 사용하는게 번거롭기 때문에 쉽게 사용할 수 있는 Messenger 클래스를 제공합니다. 외부로부터 전달된 데이터는 단일 쓰레드에서 처리되기 때문에 쓰레드에 안전하도록 구현할 필요는 없습니다. 다만 Handler에서 순차적으로 Task가 처리되기 때문에 동시에 처리할 수는 없습니다.

이 글에서는 AIDL을 이용하여 Remote Service를 구현하는 방법을 소개합니다.

이 글에서 사용된 샘플은 GitHub - RemoteServiceAidlExample에서 확인할 수 있습니다.

Remote Service 정의

Service를 구현하고 실행하려면 먼저 다음과 같이 AndroidManifest.xml에 선언해야 합니다. 이렇게 선언하면 Local Service로 실행됩니다. 즉, 이 Service는 Application의 프로세스와 동일한 프로세스에서 실행됩니다.

<application>
    ...
    <service android:name=".LocalService" />

</application>

Remote Service는 Application과 다른 프로세스에서 실행되며, 다음과 같이 android:process 속성에 프로세스 이름을 입력하면 그 이름으로 프로세스가 실행되며, 서비스는 이 프로세스에서 동작합니다.

<application>
    ...
    <service android:name=".RemoteService" android:process=":remote" />

</application>

서비스를 실행한 뒤에 adb shell 명령어로 프로세스 리스트를 확인해보면 다음과 같이 두개의 프로세스를 찾을 수 있습니다. Remote Service는 com.example.remoteservice:remote 프로세스에서 동작합니다.

$ adb shell ps -ef | grep remoteservice
u0_a140       3560  1776 4 22:32:12 ?     00:00:00 com.example.remoteservice
u0_a140       3658  1776 2 22:32:13 ?     00:00:00 com.example.remoteservice:remote

서비스 구현

서비스는 다음과 같이 구현할 수 있습니다. 코드에서 보이는 IRemoteService, IRemoteServiceCallback은 AIDL으로 자동 생성된 클래스입니다. AIDL으로 인터페이스를 정의하는 방법은 뒤에서 알아보겠습니다.

class RemoteService : Service() {

    private val binder = object : IRemoteService.Stub() {
        override fun addCallback(callback: IRemoteServiceCallback): Boolean {
            return true;
        }

        override fun removeCallback(callback: IRemoteServiceCallback?): Boolean {
            return true;
        }
    }

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

}

AIDL 인터페이스 정의

AIDL은 프로세스간 통신을 위한 인터페이스가 정의할 때 사용합니다. AIDL 파일을 만들면 정의된 인터페이스를 기반으로 바인더 코드가 자동 생성됩니다.

안드로이드 스튜디오에서 [File] -> [New] -> [AIDL] -> [AIDL File]을 누르시면 다음과 같이 AIDL 파일을 만들 수 있습니다. android studio aidl

IRemoteService, IRemoteServiceCallback을 생성하시면 다음과 같이 aidl 폴더에 파일들이 생성됩니다. android studio aidl file

파일을 열어보면 자바로 구현된 샘플 코드가 입력되어 있습니다. AIDL은 Java를 사용하여 인터페이스를 정의해야 합니다.

이제 샘플 코드를 지우고, 인터페이스를 정의해보겠습니다.

IRemoteService.aidl은 다음과 같이 코드를 변경해 줍니다.

package com.example.remoteservice;

import com.example.remoteservice.IRemoteServiceCallback;

interface IRemoteService {
    boolean addCallback(IRemoteServiceCallback callback);
    boolean removeCallback(IRemoteServiceCallback callbac);
}

IRemoteServiceCallback.aidl은 다음과 같이 코드를 변경해 줍니다.

package com.example.remoteservice;

interface IRemoteServiceCallback {
    void onItemAdded(String name);
    void onItemRemoved(String name);
}

AIDL 파일을 구현했다고 바로 코드가 자동 생성되는 것은 아닙니다. [Build] -> [Make Project]를 누르면 AIDL이 컴파일되어 코드가 자동 생성됩니다.

자동 생성된 코드는 다음 경로에 생성됩니다.

app/build/generated/aidl_source_output_dir/debug/out/com/example/remoteservice$ ls
IRemoteServiceCallback.java  IRemoteService.java

다음과 같이 자동 생성된 코드를 GitHub에 올려두었습니다. 디버깅 목적으로 로그도 추가하였습니다. 궁금하시면 여기서 간단히 코드를 확인하실 수 있습니다.

Remote Service 구현

다음은 제가 구현한 RemoteService의 전체 코드입니다. 여기서 중요한 것은 binder입니다. App이 이 Service에 바인딩할 때 binder를 리턴합니다. App은 바인더에 정의된 인터페이스를 이용하여 Service와 통신할 수 있습니다.

class RemoteService : Service() {
    companion object {
        const val TAG = "RemoteService"
    }

    val listeners = arrayListOf<IRemoteServiceCallback>()
    private val binder = object : IRemoteService.Stub() {
        override fun addCallback(callback: IRemoteServiceCallback): Boolean {
            Log.d(TAG, "Add callback : $callback")
            listeners.add(callback)
            return true;
        }

        override fun removeCallback(callback: IRemoteServiceCallback?): Boolean {
            Log.d(TAG, "Remove callback : $callback")
            listeners.remove(callback)
            return true;
        }
    }

    override fun onBind(p0: Intent?): IBinder? {
        handler.sendEmptyMessageDelayed(0, 5000)
        return binder
    }

    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            listeners.forEach { listener ->
                listener.onItemAdded("new item");
                listener.onItemRemoved("old item");
            }
            sendEmptyMessageDelayed(0, 5000)
        }
    }
}

Binder 구현

위의 코드에서 Binder 구현 코드를 좀 더 자세히 보면, Binder는 IRemoteService.Stub을 상속받습니다. IRemoteService.StubIRemoteService.aidl파일이 컴파일될 때 자동 생성되는 클래스입니다.

아래 코드는 익명 클래스로 구현했지만, 클래스를 정의하고 그것을 생성해도 됩니다.

private val binder = object : IRemoteService.Stub() {
    override fun addCallback(callback: IRemoteServiceCallback): Boolean {
        Log.d(TAG, "Add callback : $callback")
        listeners.add(callback)
        return true;
    }

    override fun removeCallback(callback: IRemoteServiceCallback?): Boolean {
        Log.d(TAG, "Remove callback : $callback")
        listeners.remove(callback)
        return true;
    }
}

Service에서 App으로 이벤트 전달

IRemoteService에 대한 바인더를 App에 전달하면, App만 서비스를 호출할 수 있습니다. 즉, 서비스는 App에게 어떤 이벤트도 전달할 수 없습니다.

만약 App이 Service에게 바인더를 전달해준다면, Service는 App에게 이벤트를 전달할 수 있습니다. 위에서 정의된 IRemoteService.StubaddCallback()은 App이 Service로 IRemoteServiceCallback 객체를 전달하기 위해 만든 메소드입니다.

만약 바인딩이 되면, 서비스가 App 쪽으로 5초 간격으로 onItemAdded(), onItemRemoved()를 호출하도록 하였습니다.

override fun onBind(p0: Intent?): IBinder? {
    handler.sendEmptyMessageDelayed(0, 5000)
    return binder
}

private val handler = object : Handler(Looper.getMainLooper()) {
    override fun handleMessage(msg: Message) {
        listeners.forEach { listener ->
            listener.onItemAdded("new item");
            listener.onItemRemoved("old item");
        }
        sendEmptyMessageDelayed(0, 5000)
    }
}

위의 예제는 단순히 Service에서 App의 바인더를 이용하여 인터페이스를 호출하는 것을 보기 위한 것입니다. 실제로는 이런 패턴으로 사용되지 않을 수 있습니다. 단순히 참고만 해주세요.

App에서 서비스 바인더 얻기

다음 코드는 App에서 서비스로 바인딩하고, IRemoteService바인더를 얻습니다. 그리고 App은 이 바인더를 통해 IRemoteServiceCallback 바인더를 서비스에 전달합니다.

class MainActivity : AppCompatActivity() {

    companion object {
        const val TAG = "MainActivity"
    }

    private var bound = false;
    private var iRemoteService: IRemoteService? = null

    private var callback = object : IRemoteServiceCallback.Stub() {
        override fun onItemAdded(name: String?) {
            Log.d(TAG, "onItemAdded: $name")
            bound = true
        }

        override fun onItemRemoved(name: String?) {
            Log.d(TAG, "onItemRemoved: $name")
            bound = false
        }
    }

    private val connection = object : ServiceConnection {
        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            Log.d(TAG, "onServiceConnected: $className")
            iRemoteService = IRemoteService.Stub.asInterface(service)
            iRemoteService!!.addCallback(callback)
        }

        override fun onServiceDisconnected(className: ComponentName) {
            Log.d(TAG, "onServiceDisconnected: $className")
            iRemoteService = null
        }
    }

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

    override fun onResume() {
        super.onResume()
        if (!bound) {
            val intent = Intent(this, RemoteService::class.java)
            bindService(intent, connection, Context.BIND_AUTO_CREATE);
        }
    }

    override fun onStop() {
        super.onStop()
        if (bound) {
            iRemoteService!!.removeCallback(callback)
            unbindService(connection)
        }
    }
}

바인딩, 바인더 변환

서비스에 바인딩하는 코드를 좀 더 자세히 알아보겠습니다.

다음과 같은 코드를 실행하면, 서비스에 바인딩을 시도합니다.

val intent = Intent(this, RemoteService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE);

서비스에 연결되면 onServiceConnected()이 callback되며 IRemoteService가 인자로 전달됩니다.

private var iRemoteService: IRemoteService? = null

private val connection = object : ServiceConnection {
    override fun onServiceConnected(className: ComponentName, service: IBinder) {
        Log.d(TAG, "onServiceConnected: $className")
        iRemoteService = IRemoteService.Stub.asInterface(service)
        iRemoteService!!.addCallback(callback)
    }

    override fun onServiceDisconnected(className: ComponentName) {
        Log.d(TAG, "onServiceDisconnected: $className")
        iRemoteService = null
    }
}

인자를 보면 IBinder로 전달됩니다. IRemoteService는 IBinder를 상속하는 구조이기 때문에, IBinder를 IRemoteService로 변환할 수 있습니다.

IRemoteService.aidl은 컴파일될 때 IRemoteService.Stub.asInterface() 메소드를 생성합니다. 다음과 같이 이 메소드를 사용하여 IBinder를 IRemoteService로 변환할 수 있습니다.

iRemoteService: IRemoteService? = IRemoteService.Stub.asInterface(service)

확인

앱을 실행하고 의도한대로 잘 동작하는지 로그로 확인해보았습니다. PID를 보시면 MainActivity와 RemoteService가 다른 프로세스에서 실행되는 것을 볼 수 있습니다. 그리고 바인더를 통해 서로에게 이벤트를 전달하는 것을 볼 수 있습니다.

08-22 12:32:58.310  5905  5905 D MainActivity: onServiceConnected: ComponentInfo{com.example.remoteservice/com.example.remoteservice.RemoteService}
08-22 12:32:58.311  5940  5959 D RemoteService: Add callback : com.example.remoteservice.IRemoteServiceCallback$Stub$Proxy@ea9718b
08-22 12:33:03.306  5905  5924 D MainActivity: onItemAdded: new item
08-22 12:33:03.307  5905  5924 D MainActivity: onItemRemoved: old item
08-22 12:33:08.313  5905  5924 D MainActivity: onItemAdded: new item
08-22 12:33:08.313  5905  5924 D MainActivity: onItemRemoved: old item

참고

댓글을 보거나 쓰려면 이 버튼을 눌러주세요.
codechachaCopyright ©2019 codechacha