이전 포스팅에서는 무엇을 공부했는가?
이전 포스팅에서는 자바에서 제공되는 파이프, 공유 메모리, 블로킹큐를 이용하여 통신하는 법과 이에대한 한계점(UI 스레드가 차단될 수 있다는) 때문에 나온 안드로이드 메시지 전달 기법(루퍼, 핸들러 사용)에 대해 공부했다.
안드로이드 멀티스레딩 포스팅은 책 ‘안드로이드 멀티스레딩’을 정리한 내용이며, 이번 포스팅에서는 챕터 5 프로세스 간 통신, 즉
- 동기 및 비동기 원격 프로시저 호출(RPCs)
- 메신저(Messenger)를 통해 메시지 통신
에 대해서 공부해볼 것이다.
시작하며
안드로이드 응용프로그램 스레드는 프로세스 메모리를 공유하는 프로세스 내에서 자주 통신을 한다. 뿐만 아니라 안드로이드 플랫폼은 바인더 프레임워크(binder framework)를 통해 프로세스 경계를 넘는 통신, 즉 프로세스 간 통신(interprocess communication(IPC)도 지원한다. 바인더 프레임워크는 스레드 사이에 공유하는 메모리 영역이 없을 때 데이터 트랜잭션을 관리한다.
일반적인 IPC 사용 사례는 인텐트, 시스템 서비스, 콘텐트 프로바이더 등 안드로이드 고수준 구성요소에 의해 처리된다.
안드로이드 RPC
IPC는 시그널, 파이프, 메시지 큐, 세마포어, 공유 메모리 같은 여러 IPC 기술을 지원하는 리눅스 OS에 의해 관리된다. 안드로이드의 변형 리눅스 커널 안에서, 리눅스 IPC 기술은 프로세스 사이의 RPC 메커니즘을 수행하는 바인더 프레임워크로 대체되었다. 이를 통해 클라이언트 프로세스는 마치 로컬에서 메서드를 실행하듯 서버 프로세스의 원격 메서드를 호출할 수 있다. 그러면 데이터가 서버 프로세스로 전달되어 스레드에서 실행되고 호출한 스레드로 결과값을 반환할 수 있게 된다.
RPC메커니즘의 단계
- 메서드 데이터 분해(marshalling)
- 원격 프로세스로 마샬링된 정보를 전송
- 원격 프로세스에 정보를 재구성(unmarshalling)
- 원래 프로세스로 반환값을 전송
안드로이드 응용프로그램 프레임 워크와 코어 라이브러리는 바인더 프레임워크와 안드로이드 인터페이스 정의언어(Android Interface Definition Language, AIDL)을 통해 프로세스 통신을 추상화 한다.
1. 바인더
원격 바인더는 응용프로그램이 다른 프로세스에서 실행하는 스레드들 사이에 함수와 데이터(메서드 호출)를 보낼 수 있게 한다. 서버 프로세스는 android.os.Binder 클래스에서 지원되는 원격 인터페이스를 정의하고, 클라이언트 프로세스 안의 스레드는 원격 객체를 통해 이 원격 인터페이스에 접근할 수 있다.
함수와 데이터 모두를 전송하는 원격 프로시저 호출을 트랜잭션(transaction)이라고 부른다. 클라이언트 프로세스가 transact 메서드를 호출하면 서버 프로세스는 onTransact 메서드로 그 호출을 받는다.
transact를 호출하는 클라이언트 스레드는 기본적으로 원격 스레드에서 onTransact 메서드의 실행이 완료될 때까지 차단된다. 트랜잭션 데이터는 프로세스 사이의 전송을 위해 바인더에 의해 최적화된 android.os.Parcel 객체로 구성된다. 즉 인수(argument)와 반환값은 Parcel 객체로 전송된다. Parcel 객체는 리터럴 인수와 android.os.Parcelable을 구현한 커스텀 객체를 포함한다.
onTransact 메서드는 바인더 스레드의 풀에 속한 스레드에서 실행된다. 이 풀은 다른 프로세스에서 들어오는 요청을 처리하기 위해서만 존재한다. 최대 16개 스레드를 가지므로 모든 프로세스에서 16개 원격 호출이 동시에 처리될 수 있다. 물론 스레드 안전을 보장하는 호출의 구현이 필요하다.
IPC는 양방향으로 동작할 수 있다. 서버 프로세스가 클라이언트 프로세스에 트랜잭션을 발행할 수 있다.
바인더는 또한 IBinder.FLAG_ONEWAY를 설정함으로써 비동기 트랜잭션을 지원한다. 이 플래그를 설정하면 클라이언트 스레드는 transact를 호출하고 즉시 반환한다. 바인더는 서버 프로세스 안의 바인더 스레드에서 onTransact 호출을 계속하지만, 해당 클라이언트 스레드로 어떤 데이터를 동기적으로 반환할 수는 없다
AIDL(Android Interface Definition Language)
어떤 프로세스가 다른 프로세스에서 접근할 수 있도록 기능(functionality)을 노출하고 싶을 때는 통신 계약(communication contract)을 정의해야한다. 기본적으로 서버는 클라이언트가 호출할 수 있는 메서드의 인터페이스를 정의해놓는다. AIDL의 인터페이스를 기술하는 가장 간단하고 일반적인 방법은 .aidl 파일에 정의하는 것이다. AIDL 파일을 컴파일 하면 IPC를 지원하는 자바 코드를 생성한다. 안드로이드 응용프로그램은 이렇게 생성된 자바 코드와 상호작용하지만, 응용프로그램에서는 인터페이스만 알고 있으면된다.
이렇게 생성된 자바 인터페이스는 모든 클라이언트 응용프로그램과 서버 응용프로그램에 포함된다. 인터페이스 파일은 데이터의 마샬링, 언마샬링뿐만 아니라 트랙잭션까지 다루는 Proxy와 Stub 두 개의 내부 클래스를 정의한다. 이처럼 AIDL의 생성은 자동으로 바인더 프레임워크를 감싸는 자바 코드를 생성하고 통신 계약을 설정한다.
클라이언트 프록시와 서버 스텁은 서버 프로세스(더 정확하게는 서버 내 스레드 플에 속한 바인더 스레드)안에서 실행되더라도 클라이언트가 지역적으로 메서드를 호출하도록 허용하는 두 응용프로그램을 대신하여 RPC를 관리한다. 서버는 실행이 스레드 안전할 수 있도록 여러 클라이언트 프로세스 및 스레드의 동시 메서드 호출을 지원해야 한다.
1. 동기식 RPC
원격 메서드 호출이 서버 프로세스에서 동시에 실행된다 하더라도 클라이언트 프로세스의 호출 스레드는 동기식(차단적) 호출처럼 체감될 것이다. 바인더 스레드에서 원격 실행이 완료되고 클라이언트로 값을 반환할 수 있으면 호출 스레드는 실행을 다시 시작한다.
모든 클라이언트 호출은 오랫동안 하나의 바인더 스레드를 점유한다. 결과적으로 여러 번의 호출은 사용할 스레드를 제한하여 바인더 스레드 풀을 한계에 이르게 할 것이다. 이 경우 원격메서드 중 하나를 호출하는 다음 스레드는 바인더 큐에 들어가고 사용 가능한 바인더 스레드가 있을 때까지 실행을 시작하는 것을 기다려야한다.
여러 클라이언트 스레드가 동시에 서버 프로세스에서 차단된 메서드를 호출하면, 바인더 스레드 풀은 사용할 수 있는 스레드가 없어지게 되어 다른 클라이언트 스레드들이 원격 호출에서 결과값을 얻는 것을 막게 된다. 서버 프로세스가 차단되어 더는 사용 가능한 바인더 스레드가 없어지면 차단된 스레드를 깨울 더 이상의 바인더 스레드가 없어진다. 그러면 서버는 바인더 스레드를 깨우는데 쓰이는 자체적인 내부 스레드에 의존해야 한다. 그렇게 하지 않으면 서버는 추가로 들어오는 호출을 처리할 수 없고 서버 메서드의 반환을 기다리는 모든 클라이언트는 영원히 차단될 것이다.
차단된 자바 스레드는 보통 인터럽트가 가능하다. 즉 다른 스레드가 차단된 스레드의 실행을 완료하기 위해 차단되어 있는 스레드를 중단시킬 수 있다. 그러나 반대로 클라이언트 프로세스의 스레드는 서버 프로세스의 스레드에 직접 접근할 수 없으므로 원격 스레드를 중단시킬수는 없다. 게다가 동기식 RPC의 반환을 기다리는 클라이언트 스레드는 인터럽트를 알아채거나 처리할 수도 없다.
AIDL은 클라이언트 프로세스가 서버 프로세스에서 동시에 메서드를 실행하도록 만든다. 스레드 안전은 인터페이스 구현의 책임이라는 동시 실행을 위한 일반 규칙이 적용된다.
2. 비동기식 RPC
동기식 RPC의 강점은 단순함, 즉 이해하기 쉽고 구현하기 쉽다는 데 있다. 그러나 이런 단순함은 호출하는 스레드가 차단될 수 있다는 위험을 가져온다. 원격 호출의 실행은 종종 클라이언트 개발자가 알지 못하는 코드에 의해 수행된다. 호출 스레드가 차단되는 시간은 원격 구현의 변화에 따라 다를 수 있다. 따라서 동기식 원격 호출은 응용프로그램의 반응성에 예상치 못한 영향을 미칠 수 있다. 일반적으로 UI 스레드에 미치는 영향은, UI 스레드에서 비동기적으로 동작하는 작업자 스레드에서 모든 원격 호출을 실행함으로써 피할 수 있다. 그러나 서버 스레드가 차단된 경우, 클라이언트 스레드도 스레드와 스레드의 모든 객체가 살아 있는 상태로 기한없이 차단된다. 이는 메모리 누수의 위험성을 가진다.
따라서 비동기식 RPC를 사용해야한다. 클라이언트 스레드는 비동기식 RPC로 트랜잭션을 시작하고 즉시 반환한다. 바인더는 서버 프로세스로 트랜잭션을 제공한 다음에 클라이언트에서 서버로의 연결을 닫는다.
비동기 메서드는 반드시 void를 반환해야한다. out 또는 inout으로 선언된 인수가 없어야 한다. 결과 값을 얻기 위해선 콜백 구현을 사용한다.
비동기식 RPC는 oneway 키워드를 붙여 AIDL안에 정의하며, 인터페이스 단계나 개별 메서드에 적용될 수 있다.
비동기식 RPC의 가장 간단한 형태는 메서드 호출 시 콜백 인터페이스를 정의하는 것이다. 콜백은 서버에서 클라이언트로 보내는 호출과 같이 역방향의 RPC다.
비동기 콜백은 바인더 스레드에서 수신된다. 따라서 콜백 구현이 클라이언트 프로세스의 다른 스레드와 데이터를 공유한다면 콜백 구현 시 스레드 안전을 보장해야 한다.
바인더를 이용한 메시지 전달
내부 프로세스의 스레드 간 통신은 Message 객체를 이용하게 되는데, Message 객체는 공유 메모리 안에 위치한다. 한편 다른 프로세스의 스레드의 통신에 Message 객체를 사용할 경우 오로지 원격 프로세스 전용으로 쓰이는 핸들러로 메시지를 보내기 위해 android.os.Messenger를 사용한다. 메신저는 클라이언트 프로세스로 메신저의 참조를 전달하거나 Messeage 객체를 보내기 위해 바인더 프레임 워크를 이용한다. 핸들러는 프로세스를 넘어 전달될 수 없고 대신 메신저가 중재자 역할을 한다.
메시지는 메신저를 이용하여 다른 프로세스 내 스레드로 보내질 수 있지만, 송신 측 프로세스(클라이언트)는 수신 측 프로세스(서버)에서 메신저의 참조를 가져올 수 있다. 이 과정은 다음 두 단계로 이루어 진다.
- 메신저 참조를 클라이언트 프로세스로 전달한다.
- 서버 프로세스로 메시지를 송신한다. 일단 클라이언트가 메신저 참조를 획득하면 종종 필요에 따라 이 단계를 반복한다.
1. 양방향 통신
프로세스 간에 전달되는 메시지는 발신 프로세스 안에 Message.replyTo의 인수로 전달되는 메신저 참조를 유지한다. Message.replyTo의 인수는 메시지로 전달할 수 있는 자료형 중 하나다.
마치며
응용프로그램에서 프로세스 간 통신 대부분은 고수준의 구성요소에 의해 내부적으로 처리된다. 그러나 필요한 경우, 바인더의 저수준 메커니즘(RPC와 Messenger)로 내려갈 수 있다. 동시에 들어오는 요청을 처리하여 성능을 높이고자 하면 RPC가 바람직하다. 그렇지 않다면 메신저가 구현하기 쉬운 방법이지만 실행은 싱글 스레드로 동작한다.
다음 포스팅은 챕터 6 — 메모리 관리 에 대한 글이 될 것이다.