안드로이드, 어디까지 아세요 [2.2] — Bound Service, IPC

Android Bound Service의 개념과 IBinder, Messenger, AIDL을 이용한 구현

MJ Studio
MJ Studio

--

안드로이드, 어디까지 아세요 시리즈의 세 번째 글입니다. 이전에 다루었던 서비스 컴포넌트를 연이어 작성하며 Bound ServiceIPCAIDL에 대해서 살펴볼 것입니다.

Bound Service

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

바인딩된 서비스는 시작된(Started)서비스와 다르게 startServicestopSelf, stopService를 이용해 생명주기를 관리하지 않습니다. Java API인 bindService를 이용해서 바인딩을 연결하거나 C/C++, HAL 쪽에서 바인딩을 연결하면 서비스가 생성되고(onCreate), 바인딩된 클라이언트가 모두 해제되었을 때 파괴됩니다(onDestroy).

서비스를 바인딩 목적으로만 사용하려면 onStartComand를 구현할 필요없이 onBind 메소드에서 IBinder만 반환하면 됩니다. 그러면 이 IBinder객체를 클라이언트측에서 얻어 서비스와 커뮤니케이션을 합니다.

Bound Service는 bindService, unbindService 메소드를 이용해 클라이언트(Java API를 사용하는 안드로이드 컴포넌트)측에서 연결, 해제를 진행할 수 있습니다. 마지막 클라이언트가 바인딩을 해제하면 서비스는 파괴될 것입니다(시작된 상태가 아닐 때).

Started Service와 Bound Service는 앞서 언급했듯이 별개의 것이 아닌 동시에 두 가지 목적으로 서비스가 구성될 수도 있습니다. 그러나 굳이 그럴 필요는 많지 않을 것입니다.

서비스는 여러 클라이언트와 동시에 바인딩될 수 있습니다. 그러나 onBind로 반환된 IBinder 객체는 첫 onBind 메소드에서만 생성되고 계속 동일한 녀석이 캐시되어 사용됩니다. 즉, onBind 메소드는 처음 한 번만 호출됩니다.

IBinder 구현법, Bound Service의 구현 종류

IBinder 객체를 얻는 방법은 총 세가지가 있습니다.

Binder 클래스 상속

서비스가 다른 앱이나 프로세스와 커뮤니케이션(IPC)를 수행할 필요가 없다면, 단순하게 Binder 추상 클래스를 상속하여 그 객체를 만들고 IBinder 객체를 onBind에서 반환할 수 있습니다. 예를 들어, inner class로 바인더 클래스를 만들고 서비스를 참조할 수 있는 함수를 포함하게 한 후 onBind에서 그 객체를 반환하면, 다른 컴포넌트에서 Service에 접근해서 서비스의 함수들을 사용할 수 있습니다.

Messenger

서비스를 이용해 IPC를 구현하는 방법 중 쉬운 방법입니다. 안드로이드에서 쓰레드를 사용할 때도 사용되는 Handler, Message를 이용해 통신이 이루어지는데, Handler를 참조하는 Messanger라는 클래스의 객체를 만들어 IBinder를 만들고 참조할 수 있습니다. 자세한 구현법은 곧 예제와 함께 만나보겠습니다.

AIDL

AIDLAndroid Interface Definition Language의 준말입니다. 흔히 IDL이라고 하면 서로 다른 두 프레임워크나 언어 환경 간 공통적으로 이해할 수 있는 명세서를 적는 인터페이스를 가리키곤 합니다. 앞서 살펴본 Messenger는 내부적으로 AIDL을 이용합니다. Messenger하나의 쓰레드에서 모든 클라이언트들의 요청을 한번에 처리하는데, AIDL은 여러 요청을 다중 쓰레드(multi thread)에서 동시에 실행이 가능합니다.

AIDL을 사용하려면 .aidl 파일을 만들어야 하며, 이걸 만들면 시스템은 우리가 직접 구현할 수 있는 추상 클래스를 하나 생성합니다.

도큐먼트에서는 AIDL은 대부분의 어플리케이션에서 사용될 필요가 없다고 설명합니다. 다중 쓰레드 환경의 코드를 짜는것은 더 복잡하며 실수하기 쉽고 대부분의 use-cases를 Messenger를 이용해 구현할 수 있기 때문입니다.

Usage: 서비스를 바인딩하는 법

https://developer.android.com/guide/components/bound-services#Binding

실제 코드 수준에서 어떻게 서비스를 바인딩할 수 있는지 살펴보겠습니다. 이는 Android framework Java API에서 ServiceConnection 객체를 이용해 바인딩하는 방법을 다룹니다.

우선, startService 로 서비스를 시작하듯이 bindService 를 이용해 바인딩을 합니다. 그러면 서비스의 onBind 메소드가 호출됩니다. 바인딩 과정은 비동기적으로 이루어지기 때문에 bindService는 바로 반환되고, 두 번째 인자로 전달한 ServiceConnection 객체의 콜백으로 바인딩된 IBinder 객체를 전달받게 됩니다.

서비스는 Activity, Service, ContentProvider 에서 바인딩이 가능하지만 BroadcastReceiver 에서는 바인딩이 불가능합니다.

Example: Basic Binder extended Service

실제 코드로 살펴보겠습니다. 우선 MyBinderExtendedService 라는 서비스를 만들겠습니다.

MyBinder 라는 Binder 를 상속하는 클래스를 inner class로 만들어서 서비스 객체 그 자체를 반환할 수 있게 해주었습니다. 그리고 액티비티에서는 다음과 같은 코드로 서비스를 바인딩할 수 있습니다.

bindService 메소드의 두 번째인자는 ServiceConnection 객체이고 세 번째 인자는 보통 BIND_AUTO_CREATE 가 되어야 하는 서비스 실행 관련 플래그입니다. ServiceConnection 객체의 초기화 코드를 보겠습니다.

이렇게 Binder 객체를 얻어와 갖고 있다면, 서비스를 가져와서 MyBinderExtendedService에 선언된 토스트를 보여주는 메소드를 호출할 수 있게 됩니다.

binderExtendedServiceBinder?.run {
service.showToast("Service is bound!")
}

Example: Messenger IPC Service

Messenger 를 사용하는 서비스를 같은 프로세스 안에서도 사용이 가능하지만, 서로 다른 어플리케이션에서 실행되는 IPC 예제를 만들기 위해 <service> 태그의 process attribute를 설정해서 서비스를 개별 프로세스에서 실행되게 하겠습니다.

<service
android:name=".MyMessengerIPCService"
android:process=":my_process"
android:enabled="true"
android:exported="true" />

이 상태로 서비스가 생성되면 다음과 같이 Logcat에서 프로세스가 따로 분리되어 실행된 것을 확인 가능합니다.

서비스 코드

코드가 조금 복잡하지만 하나하나 살펴보겠습니다.

  1. companion object로 IPC 과정에서 Message 의 what 이나 data에 담길 상수들을 정의합니다.
  2. Handler 를 하나 만들고 그 안에 핸들러가 처리해줄 동작들을 정의해줍니다. Message 객체의 what 인자를 이용합니다. 이 과정에서 replyTo 로 넘어온 클라이언트를 의미하는 Messenger 객체들을 저장/삭제 하는 것에 유의하세요.
  3. onBind 에서 Messenger 객체를 만들어서 binder 속성을 반환합니다.

이제 서비스쪽은 모두 살펴보았고, 클라이언트(예제에선 단순히 MainActivity)를 살펴보겠습니다.

클라이언트 코드

우선 Messenger 나 ServiceConnection 들을 다루는 선언부를 보겠습니다.

서비스쪽에서도 Handler를 정의해주고 그걸 Messenger 객체로 만들어주었지만, 서비스쪽에서 clients 라는 변수에 클라이언트 측 Messenger 들을 저장해둔 것을 기억하시나요? 여기서 정의한 messengerIPCClient Messenger 객체가 바로 그 replyTo 에 넘겨줄 녀석입니다. onServiceConnected 에서 messengerIPCService Messenger 객체를 생성하며 MSG_BIND_CLIENT 라는 what 을 가진 Message 를 만들어 바로 서비스로 보내고 클라이언트측 Messenger를 등록해줌을 알 수 있습니다.

messengerIPCClient는 클라이언트 측 Messenger, messengerIPCService는 서비스 측 Messenger 라고 이해하시면 됩니다.

이제 바인딩, 언바인딩을 하는 부분과 함수를 호출하는 부분을 보겠습니다.

unbind 과정에 서비스로 replyTo 에 클라이언트 Messenger 객체를 담아서 메세지를 전달해주는 과정밖에 변한건 없습니다. 그리고 함수들을 호출해줄 때도 MyMessengerIPCService 클래스에 정의한 상수들을 사용해서 인자를 채워서 보내주기만 하면 됩니다. MSG_ADD_REQUEST 로 보낸 add 함수의 응답은 MSG_ADD_RESPONSE 로 클라이언트측 Messenger 에서 받는 것입니다.

AIDL

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

AIDL은 앞서 언급했듯이 IPC를 수행하기 위해 서비스와 클라이언트간 이해할 수 있는 자료형과 명세서로 서비스의 동작을 명시해놓는 것입니다. 이런 작업이 필요한 이유는 지금까지는 Java(Kotlin) 끼리의 바인딩만 살펴보았지만 실제로는 다른 언어가 될 수도 있기 때문입니다.

실제 서비스를 구현할 때 AIDL을 쓸일은 많지 않습니다. IPC를 구현하고 싶고 다중 쓰레드를 사용하고 싶을 때가 그렇게 많지는 않을 것입니다. 우리는 AIDL 을 이용해 함수 호출이 일어날 때, 쓰레드에 대해 쉽게 가정하면 안됩니다. 동일 프로세스내의 AIDL 호출은 호출한 쓰레드에서 실행이 되어서 쉽게 유추 가능합니다(그러나 이런 경우 AIDL이 필요하지 않겠죠). 그러나 다른 프로세스내의 IPC라면 어떤 쓰레드인지 유추가 불가능하기 때문에 AIDL 인터페이스의 구현은 무조건 thread-safe 해야합니다.

AIDL 파일

AIDL 파일은 .aldl 이란 확장자를 가진 파일입니다. src/ 디렉토리에 만들 수 있고

이렇게 안드로이드 스튜디오 New 메뉴에서 만들 수 있고 하나의 Source set에 포함되어야 합니다.

이렇게 생성된 .aldl 파일은 서비스 프로젝트와 클라이언트 프로젝트 모두에 포함되어야 하며 서비스 프로젝트에선 AIDL 파일로부터 자동으로 gen/ 디렉토리에 만들어진 Binder 를 상속하는 Stub 라는 이름의 inner class 를 구현해야 합니다. 직접 살펴보면 이해가 될 것입니다.

AIDL 파일 만들기

AIDL 파일은 간단한 문법을 사용하며 인터페이스답게 함수들을 인자, 반환형과 함께 정의 가능합니다. 기본적으로 지원하는 타입들은 다음과 같습니다.

  • 모든 자바의 원시형 (int, long, char, boolean …)
  • 원시형 타입들의 배열, int[] 같은
  • 문자열 (String, CharSequence)
  • List, Map: 지원되는 타입들이나 다른 AIDL로부터 파생된 인터페이스나 우리가 정의한 parcelables 을 타입으로 가져야 함

그리고 다음과 같이 주의할 점들이 있습니다.

  • 모든 원시형이 아닌 자료형들이 함수의 인자로 쓰일 땐, in, out, inout(directional tags) 으로 데이터의 방향을 명시해주어야 합니다.
  • 원시형, String, IBinder와 AIDL 로 만들어진 인터페이스는 in 이 기본이고 다른것은 불가능합니다.
  • Directional tag는 항상 가장 최소한의 use case 범위만 포함해야 좋습니다. 인자를 marshalling 하는 것은 리소스가 큰 작업입니다.
  • String, int형 상수는 .aidl 파일안에 선언될 수 있습니다.
  • nullable한 반환형과 리턴형은 모두 @nullable 로 어노테이트 되어야 한다.

AIDL 파일 구현하기

제가 만든 .aidl 파일입니다.

// IMyAidlInterface.aidl
package happy.mjstudio.service;

// Declare any non-default types here with import statements

interface IMyAidlInterface {
int add(int n1, int n2);
}

IMyAidlInterface.aidl 이라는 파일인데 이러면 빌드를 한번 하면 자바에도 똑같이 IMyAidlInterface.java 라는 인터페이스가 생기고 inner classBinder를 상속하는 Stub라는 클래스가 생성됩니다. 우리는 그 Stub 클래스를 구현하면 됩니다.

저는 다음과 같이 구현했습니다. 간단하죠?

private val binder = object : IMyAidlInterface.Stub() {
override fun add(n1: Int, n2: Int): Int {
return n1 + n2
}
}

구현해야 할때 주의점은 다음과 같은 것들이 있습니다. 주로 쓰레딩과 관련되어 있습니다.

  • 함수 호출이 진행되는 쓰레드를 보장할 수 없습니다. 그래서 다중 쓰레드처리나 메인 쓰레드에서만 할 수 있는 작업들을 바로 실행시키지 않게 주의합니다.
  • RPC 호출이 동기적이여서 오래걸리는 작업이라면 메인 쓰레드에서 실행된다면 ANR이 일어날 수 있으므로 별도의 쓰레드 관리가 필요할 수도 있습니다.
  • 어떠한 예외도 caller(client) 사이드로 반환되지 않습니다.

Example: AIDL IPC Service

우선 .aidl 파일은 다음과 같습니다.

// IMyAidlInterface.aidl
package happy.mjstudio.service;

// Declare any non-default types here with import statements

interface IMyAidlInterface {
int add(int n1, int n2);
}

그리고 서비스는 다음과 같이 구현합니다. 간단합니다.

클라이언트(MainActivity) 측에서는 다음과 같이 aidl 객체를 StubasInterface 메소드를 이용해 onServiceConnected 에서 설정해주면 됩니다.

그리고 바인딩, 언바인딩 코드도 동일하고 aidl 인터페이스 객체를 이용해서 바로 동기적으로 코드를 호출해줄 수 있습니다.

주의할 점은 연결이 해제되었을 때 DeadObjectException를 처리해야 하고 서비스와 클라이언트 간 AIDL명세가 올바르게 일치하지 않을 때 SecurityException을 처리해야 한다는 것입니다.

c++에관한 부분은 살펴보지 않겠지만 다음과 같은 글을 참고하면 aidl-cpp 에대한 이해를 할 수 있을 것입니다.

결론

이렇게 해서 길었던 Android Service에 대한 글을 마무리 짓게되었습니다. IPC 서비스를 사용할 때 제 경우엔 다른 어플리케이션에서 Intent의 ComponentName 을 설정하는 부분이 제대로 동작되지 않아 불가피하게 같은 패키지에서 작업할 수밖에 없었습니다. 이에 대한 해결책을 아신다면 피드백주시면 감사하겠습니다.

안드로이드의 서비스는 최근에는 개발자들이 특수한 경우가 아니면 많이 사용하지 않지만 4대 컴포넌트중 하나로써 그 동작방식과 개념에대해서는 기본적으로 이해를 하고 있어야 한다고 생각해서 글을 작성하게 되었습니다.

긴 글 읽어주셔서 감사합니다.

다음 안드로이드, 어디까지 아세요 시리즈로 돌아오겠습니다. 🚀

--

--