IPC, RPC, Binder에 대해서
Thread와 Process에 대해서는 안드로이드의 Thread와 Process 포스팅을 참고할 것.
리눅스는 서로 다른 프로세스의 메모리 영역에 접근할 수 없다. 따라서 직접적으로 함수를 불러오는 것이 불가능하다.
이것을 가능하게 하기 위해서는 커널의 공유메모리를 사용하여 프로세스간 통신을 해야 하는데 안드로이드는 이를 위해 바인더 프레임워크 (Binder framework)를 만들었다.
바인더에는 프로세스간 통신을 가능하게 하는 IPC(Inter Process Communication) 기술과 프로세스간 함수를 호출하는 RPC(Remote Procedure Call) 기술을 적용했다.
1. 안드로이드의 RPC
리눅스 OS에서는 IPC 기술을 지원하는 signal, pipe, message queue, semaphores, shared memory 이용해 IPC를 지원한다.
안드로이드의 변형 리눅스 커널에서 IPC는 RPC매커니즘을 수행하는 바인더 프레임워크로 대체되었다.
바인더 프레임워크를 이용하면 클라이언트 프로세스는 마치 로컬에서 메서드를 실행하듯이 서버 프로세스의 원격 메서드를 호출할 수 있다.
전체 과정은 아래와 같다.
- Process A에서 Process B에 있는 method(int, int)를 호출
- Process A에서 RPC 기술을 이용해 method(int,int)를 분해하여 직렬화(마샬링-marshalling)
- IPC 기술을 이용해 커널의 공유메모리를 통해 프로세스간 통신
- Process B에서 RPC 기술을 이용해 method(int,int)를 조립(언마샬링-unmarshalling)
- Process B에 있는 method(int,int)를 실행
2. 바인더 프레임워크가 필요한 이유
안드로이드에서 리눅스 커널이 기본적으로 제공하는 소켓, Pipe 등과 같은 IPC를 사용하지 않고 바인더 메커니즘을 새로 만든 이유는 성능 때문이다.
모바일 기기를 지원하기 위한 안드로이드의 모든 시스템 기능은 프로세스로 제공된다.
예를 들어 내가 만든 응용프로그램에서 Android SDK가 제공하는 위치 정보를 얻는 API를 호출할 때 내부적으로는 Location 서비스를 제공하는 Linux 프로세스로 요청을 보내고 결과를 응답받아 처리한다. 카메라를 사용할 때도 마찬가지로 Camera 서비스와 상호 연동한다.
이렇게 Android의 모든 시스템 기능이 서버 프로세스로 제공되기 때문에 프로세스 사이에 최적화된 통신 방법이 필요하고 그 고민의 결과가 바인더이다.
3. 바인더
바인더는 앱이 다른 프로세스에서 실행되는 스레드들 사이에 메서드 호출을 보낼 수 있게 한다.
서버 프로세스는 android.os.Binder
클래스에서 지원되는 원격 인터페이스를 정의하고, 클라이언트 프로세스 안의 스레드는 원격 객체를 통해서 이 원격 인터페이스에 접근할 수 있다.
- 함수와 데이터를 모두 전송하는 원격 프로시저 호출을 트랜잭션 이라고 부른다.
위 그림과 같이 클라이언트 프로세스가 transact()
메서드를 호출하면 서버 프로세스는 onTransact()
메서드를 통해 호출을 받는다.
기본적으로 transact()
를 호출하는 클라이언트 프로세스의 쓰레드는 메서드 호출 후 onTransact()
호출이 완료될때까지 차단되기 때문에 동기로 동작하게 된다.
3.1. Parcel 객체
트랜잭션 데이터는 android.os.Parcel
객체로 구성된다.
이 객체는 리터럴 파라미터와 android.os.Parcelable
을 구현한 커스텀 객체를 포함할 수 있는데 Parcelable 인터페이스는 Serializable보다 효율적인 방법으로 마샬링, 언마샬링을 지원하게 한다.
3.2. Transaction Thread pool
서버 프로세스에서 onTransact()
메서드는 Binder Thread pool에 속한 스레드에서 실행된다.
이 바인더 쓰레드는 프로세스간 통신을 위해서만 사용하는 쓰레드이다. OS 버전에 따라 다를 수 있지만 풀은 최대 16개의 쓰레드를 가지고 있어 총 16개의 원격 호출이 동시 처리될 수 있다.
3.3. 비동기 Transaction
바인더 통신은 기본적으로는 동기로 동작하는데 IBinder.FLAG_ONEWAY
flag를 설정하여 비동기로 호출할 수도 있다.
이 때 클라이언트 쓰레드는 tranact()
메서드 호출 시 즉시 반환받는다. 서버 프로세스의 쓰레드는 onTransact()
메서드 호출을 받지만 클라이언트 쓰레드에게 어떤 데이터를 동기적으로 반환해줄 수는 없다.
3.4. 같은 앱 내에서의 Bind 통신
만약 같은 앱 내부에서 bindService를 사용하게 되면 Binder Thread를 사용하지 않는다. Binder thread는 프로세스간 통신을 위한것인데 같은 프로세스 내라면 이와 같은 불필요한 작업 없이도 가능하기 때문이다.
이같은 경우 aidl 파일도 필요없고 그냥 LocalBinder를 만들어 주면 된다.
4. AIDL
앱에 바인더를 만드는것은 복잡한 작업인데 이를 쉽게 하기 위해 안드로이드는 인터페이스 정의 언어인 AIDL(Android Interface Definition Language)을 제공하고, 이 언어로 인터페이스를 작성하면 자동으로 바인더를 생성해준다.
즉, 앱에서 AIDL을 정의해두면 컴파일 시 바인더 프레임워크를 랩핑하는 자바 코드를 자동으로 생성한다.(gen 폴더에 생성됨)
aidl 파일은 Interface를 정의하는것처럼 서비스가 제공하는 함수를 정의하면 되고, 파일은 꼭 .aidl로 생성해야 한다.
참고로 aidl파일을 바인더 클래스로 생성해주는 작업은 Android SDK에 포함된 aidlTool이다.
4.1. Proxy, Stub을 통한 원격 프로시저 호출
AIDL로 바인더 클래스를 자동 생성하면 내부에 Inner클래스로 Stub과 Proxy 클래스가 존재한다.
- Proxy : 클라이언트에서 실행되는 코드로 호출하려는 함수를 분해(마샬링)하여 전송한다.
- Stub : 서버에서 실행되는 코드로 제공하려는 함수를 조립하여(언마샬링) 호출한다.
4.2. 동기식 RPC
간단한 예를 통해 동기식 RPC를 이해한다.
-
AIDL 정의
interface ISynchronous { String getThreadNameFast(); String getThreadNameSlow(long sleep); String getThreadNameBlocking(); String getThreadNameUnblock(); }
-
서버 프로세스에서 Stub 클래스 오버라이드
private final ISynchronous.Stub mBinder = new ISynchronous.Stub() { CountDownLatch mLatch = new CountDownLatch(1); @Override public String getThreadNameFast() throws RemoteException { return Thread.currentThread().getName(); } @Override public String getThreadNameSlow(long sleep) throws RemoteException { SystemClock.sleep(sleep); return Thread.currentThread().getName(); } @Override public String getThreadNameBlocking() throws RemoteException { mLatch.await(); return Thread.currentThread().getName(); } @Override public String getThreadNameUnblock() throws RemoteException { mLatch.countDown(); return Thread.currentThread().getName(); } }
-
클라이언트 프로세스에서 원격 메서드 호출
ISynchronous mISynchronous = ISynchronous.Stub.asInterface(binder); String remoteThreadName = mISynchronous.getThreadNameFast(); Log.d(TAG, "Thread Name : " + remoteThreadName); // 결과는 "Thread Name : Binder_1" 출력됨.
-
빨리 리턴하는 작업 호출
getThreadNameFast()
호출은 즉시 리턴되므로 호출하는 클라이언트는 아주 잠깐 block 되고 바인더 쓰레드는 효율적으로 재활용 될 수 있다. -
시간이 오래걸리는 작업 호출
getThreadNameSlow(sleep)
호출은 요청받은 만큼 긴 시간동안 클라이언트가 block 된다. 이 때 오랫동안 하나의 바인더 쓰레드를 점유하게 되기 때문에 여러번 호출할 경우 쓰레드 풀이 한계에 이르게 된다.
쓰레드 풀이 한계에 다다르면 다음 원격 메서드 호출은 바인더 큐에 들어가게 되고 사용가능한 바인더 쓰레드가 있을때까지 실행을 기다리게 된다.
- Block되는 메서드 호출
getThreadNameBlocking()
호출 시 바인더 쓰레드는 block 되고 클라이언트 쓰레드 역시 block된다.
이 호출만 여러번 되면 결국 쓰레드 풀이 한계에 다다르게 되는데 그러면 이 block을 풀어줄 getThreadNameUnblock()
을 외부에서 호출해줄 수 없게 된다. 그럴 경우에는 block 된 쓰레드를 풀어주는 기능을 서버 프로세스 내부 쓰레드에 의존해야 한다.
그렇지 않으면 단말에서 원격 메서드를 호출하는 모든 클라이언트 쓰레드가 block된다.
원격 메서드 호출이 빠르게 리턴된다고 해서 클라이언트의 메인 쓰레드에서 호출하는것이 안전하다고 할 수는 없다. 서버 프로세스가 실행되는 시간을 단정지을 수 없기 때문이다.
4.3. 비동기식 RPC
동기식 RPC는 단순하고 구현하기 쉬운 장점이 있지만 호출하는 클라이언트 쓰레드가 차단될 수 있다는 위험이 있다.
비동기식 RPC를 사용하면 클라이언트가 자신의 비동기 정책을 구현하는 대신 원격 메서드 호출 자체를 비동기로 실행하도록 정의할 수 있다.
비동기로 실행 시 바인더는 서버 프로세스로 트랜잭션을 제공한 다음 클라이언트와 서버간의 연결을 닫는다.
즉, 서버 프로세스의 원격 메서드 호출 결과를 클라이언트에 전달할 수 없는데 이 때문에 원격 메서드는 반드시 void를 리턴해야 한다. 만약 결과를 전달하기 위해서는 콜백 구현을 사용해야 한다.
4.3.1. 비동기식 RPC 정의
비동기식 RPC는 oneway 키워드를 붙여 AIDL 안에 정의한다.
oneway는 인터페이스 단계에 정의할 수도 있고, 개별 메서드 단계에서 정의할 수도 있다.
oneway interface IAsynchronousInterface {
void method1();
void method2();
}
interface IAsynchronousInterface {
oneway void method1();
void method2();
}
4.3.2. 비동기식 RPC의 콜백 구현
콜백을 보낸다는 것은 서버에서 클라이언트로 호출을 보내는 역방향 RPC를 의미한다.
이 역시 RPC이므로 콜백 인터페이스는 AIDL에 정의가 필요하다.
-
원격 메서드 AIDL 정의
interface IAsynchronous1 { oneway void getThreadNameSlow(IAsynchronousCallback callback); }
-
원격 메서드의 콜백 메서드 AIDL 정의
interface IAsynchronousCallback { void handleResult(String name); }
-
서버 프로세스에서 Stub 클래스 오버라이드
IAsynchronous1.Stub mIAsynchronous1 = new IAsynchronous1.Stub() { @Override public void getThreadNameSlow(IAsynchronousCallback callback) throws RemoteException { SystemClock.sleep(10000); String threadName = Thread.currentThread().getName(); callback.handleResult(threadName); } }
-
클라이언트 프로세스에서 콜백을 위한 Stub 클래스 오버라이드
IAsynchronousCallback.Stub mCallback = new IAsynchronousCallback.Stub() { @Override public void handleResult(String remoteThreadName) throws RemoteException { Log.d(TAG, "Remote thread Name : " + remoteThreadName); Log.d(TAG, "Current thread Name : " + Thread.currentThread().getName()); } }
참고로 둘 다 동일한 쓰레드 이름이 출력될수 있지만 두 쓰레드는 각각 클라이언트 프로세스에 속한 바인더 쓰레드와 서버 프로세스에 속한 바인더 쓰레드이다.
바인더 콜백은 바인더 쓰레드에서 수신되니 콜백 구현이 클라이언트 프로세스의 다른 쓰레드와 데이터 공유가 필요하다면 thread safe에 주의해야 한다.
[참고 문서]
- 네이버 개발자 블로그
- 도서 “이것이 안드로이드다”
- 도서 “Efficient Android Threading”
댓글남기기