자동차의 시동을 켜는 순간, 수많은 시스템이 즉시 깨어나 운전자를 맞이합니다. 내비게이션, 미디어 플레이어, 공조 시스템 등은 모두 보이지 않는 곳에서 동작하는 서비스들의 조화로운 오케스트라 덕분입니다. Android Automotive OS(AAOS) 환경에서 이러한 핵심 서비스를 개발하는 개발자에게 가장 중요한 과제 중 하나는 바로 '차량 부팅 시 내가 만든 서비스를 어떻게 안정적으로, 그리고 가장 적절한 시점에 시작할 것인가?'입니다. 이 질문은 단순한 자동 실행을 넘어, 시스템의 안정성, 성능, 그리고 사용자 경험과 직결되는 문제입니다.
일반적인 안드로이드 앱 개발에서 사용하는 BOOT_COMPLETED 브로드캐스트 수신은 AAOS의 특수한 환경에서는 충분하지 않을 수 있습니다. 차량의 핵심 기능과 연동되는 서비스는 사용자 공간의 앱보다 훨씬 더 빨리, 시스템의 핵심 구성 요소들과 함께 초기화되어야 하기 때문입니다. 예를 들어, 차량의 CAN(Controller Area Network) 데이터를 수집하여 분석하는 서비스나, 커스텀 하드웨어를 제어하는 서비스는 시스템 부팅 초기 단계부터 실행 준비를 마쳐야 합니다.
본 가이드에서는 AAOS 환경에서 시스템 서비스를 부팅 시점에 자동으로 실행하는 다양한 방법을 심층적으로 분석하고, 각 방법의 장단점과 적용 시나리오를 구체적인 코드와 함께 제시합니다. 평범한 앱 개발의 관점을 넘어, AOSP(Android Open Source Project)를 직접 수정하여 시스템의 심장부인 SystemServer에 서비스를 등록하는 가장 확실하고 강력한 방법까지 다룰 것입니다. 이 글을 통해 여러분은 AAOS 시스템 개발의 핵심 역량을 한 단계 끌어올릴 수 있을 것입니다.
시스템 서비스와 일반 앱 서비스의 근본적인 차이
본격적인 논의에 앞서, 왜 AAOS에서 '시스템 서비스'라는 개념이 중요한지 짚고 넘어가야 합니다. 일반적인 안드로이드 앱의 서비스와 시스템 서비스는 실행 권한, 생명 주기, 그리고 시스템과의 상호작용 방식에서 하늘과 땅 차이입니다.
AAOS에서 우리가 만들고자 하는 서비스가 다음과 같은 특징을 가진다면, 그것은 시스템 서비스로 구현되어야 합니다.
- 하드웨어 직접 제어: 차량의 특정 센서, ECU(Electronic Control Unit), 조명 등 하드웨어에 직접 접근해야 하는 경우.
- 높은 수준의 권한 필요: 일반 앱에 허용되지 않는 시스템 설정을 변경하거나, 다른 앱의 동작에 영향을 미치는 기능을 수행해야 하는 경우. (예:
SignatureOrSystem보호 수준의 권한) - 부팅 초기 실행 보장: 다른 모든 앱과 서비스가 시작되기 전, 시스템 초기화 단계에서 반드시 실행되어야 하는 경우.
- 안정적인 실행: 사용자가 앱을 종료하거나 메모리 부족 상황에서도 절대 종료되어서는 안 되는 핵심 기능을 담당하는 경우.
이러한 서비스를 구현하기 위해서는 단순히 APK를 설치하는 것을 넘어, 안드로이드 플랫폼 소스 코드를 직접 빌드하고, 우리의 앱을 '특권 앱(Privileged App)' 또는 '시스템 앱(System App)'으로 만들어야 합니다. 이는 /system/app 또는 /system/priv-app 파티션에 앱을 설치하고, 플랫폼의 서명 키로 서명하는 과정을 포함합니다. 이 과정을 통해 우리의 앱은 시스템의 일부로 인정받게 되며, 비로소 시스템 서비스를 등록할 자격을 얻게 됩니다.
방법 1: ACTION_BOOT_COMPLETED - 가장 간단하지만 충분하지 않은 방법
가장 널리 알려진 안드로이드 부팅 시점 실행 방법은 android.intent.action.BOOT_COMPLETED 브로드캐스트를 수신하는 것입니다. 시스템 부팅이 완료되고 사용자가 기기를 사용할 수 있는 상태가 되면, 시스템은 이 인텐트를 방송합니다. 개발자는 BroadcastReceiver를 등록하여 이 신호를 받고, 서비스 시작과 같은 원하는 작업을 수행할 수 있습니다.
구현 방법
구현은 매우 간단합니다. 먼저, AndroidManifest.xml 파일에 Receiver를 등록하고 필요한 권한을 선언합니다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mysystemservice">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- 서비스 실행을 위한 추가 권한 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
...>
<receiver
android:name=".BootCompletedReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<service
android:name=".MyVehicleService"
android:enabled="true"
android:exported="false" />
</application>
</manifest>
다음으로, BroadcastReceiver 클래스를 구현하여 인텐트를 수신했을 때 서비스를 시작하는 코드를 작성합니다.
package com.example.mysystemservice;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.util.Log;
public class BootCompletedReceiver extends BroadcastReceiver {
private static final String TAG = "BootCompletedReceiver";
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Log.d(TAG, "Boot completed event received. Starting service...");
Intent serviceIntent = new Intent(context, MyVehicleService.class);
// Android O (API 26) 이상에서는 Foreground Service로 시작해야 함
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(serviceIntent);
} else {
context.startService(serviceIntent);
}
}
}
}
AAOS 환경에서의 한계점
이 방법은 구현이 쉽고 간단하다는 명확한 장점이 있지만, AAOS의 시스템 레벨 서비스를 만들기에는 여러 가지 치명적인 한계가 있습니다.
- 느린 시작 시점:
BOOT_COMPLETED는 Zygote가 앱 프로세스들을 생성하고, 시스템 서버의 핵심 서비스들이 대부분 로드된 후, 심지어 홈 화면이 표시된 이후에야 방송됩니다. 차량의 핵심 제어 로직을 담는 서비스에게는 너무 늦은 시점입니다. 부팅 애니메이션이 나오는 동안 이미 내부적으로는 차량 상태를 점검해야 할 수도 있습니다. - 실행 보장의 불안정성: 이 방법으로 시작된 서비스는 여전히 일반적인 앱 서비스의 생명 주기를 따릅니다. 시스템의 메모리가 부족해지면 Low Memory Killer에 의해 언제든지 종료될 수 있습니다. 차량의 안전과 관련된 중요한 서비스가 메모리 부족으로 종료된다면 상상만 해도 끔찍한 일입니다.
- 권한의 제약:
BOOT_COMPLETED를 수신하여 시작된 서비스는 해당 앱이 가진 권한 내에서만 동작합니다. 차량 내부의 저수준(low-level) 하드웨어나 보호된 시스템 API에 접근하기 위한SignatureOrSystem권한을 사용하는 데에는 한계가 있습니다.
이 방법은 차량의 핵심 기능과는 무관하지만, 부팅 후 자동으로 실행되어야 하는 사용자 편의 기능(예: 부팅 후 마지막 미디어 자동 재생) 등에 제한적으로 사용될 수 있습니다. 하지만 우리가 만들고자 하는 진정한 '시스템 서비스'와는 거리가 멉니다. AAOS 개발자
방법 2: SystemServer에 직접 서비스 등록 - 가장 확실하고 강력한 방법
차량의 핵심 기능을 담당하는 서비스를 가장 이른 시점에, 가장 안정적으로 실행하는 방법은 바로 안드로이드의 심장인 SystemServer 프로세스에 직접 서비스를 등록하는 것입니다. 이 방법은 AOSP 소스 코드를 직접 수정해야 하므로 복잡성이 높지만, 그만큼 확실한 제어권과 안정성을 보장합니다.
SystemServer는 안드로이드 부팅 시 Zygote에 의해 생성되는 첫 번째 Java 프로세스로, 시스템의 모든 핵심 서비스(Activity Manager, Power Manager, Package Manager 등)를 생성하고 관리합니다. 여기에 우리만의 서비스를 추가함으로써, 안드로이드 프레임워크의 일부처럼 동작하게 만들 수 있습니다.
전체 과정은 다음과 같은 단계로 이루어집니다.
- AIDL 인터페이스 정의: 서비스가 외부에 제공할 기능(API)을 AIDL(Android Interface Definition Language)로 정의합니다.
- 서비스 구현: AIDL 인터페이스를 구현하는 실제 서비스 클래스를 작성합니다.
- AOSP 빌드 시스템 설정 (Android.bp): 서비스를 AOSP의 일부로 빌드하기 위한 빌드 스크립트를 작성합니다.
- SystemServer에 서비스 등록:
SystemServer.java파일을 수정하여 부팅 과정에서 우리 서비스를 시작하도록 코드를 추가합니다. - SELinux 정책 추가: 서비스가 필요한 시스템 리소스에 접근할 수 있도록 SELinux 정책을 설정합니다.
이제 각 단계를 상세히 살펴보겠습니다.
1단계: AIDL 인터페이스 정의
시스템 서비스는 다른 시스템 컴포넌트나 특권 앱과 통신해야 할 수 있습니다. 이를 위해 프로세스 간 통신(IPC) 메커니즘인 Binder와, 그 인터페이스를 쉽게 정의하게 해주는 AIDL을 사용합니다. 예를 들어, 차량의 특정 부품(예: 선루프)을 제어하는 서비스를 만든다고 가정해 봅시다.
packages/apps/MyCarSystem/src/com/example/mycarsystem/IMyCarSunroofService.aidl 파일을 생성합니다.
package com.example.mycarsystem;
/**
* 선루프 제어를 위한 시스템 서비스 인터페이스
*/
interface IMyCarSunroofService {
/**
* 선루프를 엽니다.
* @param percentage 0에서 100까지의 열림 정도
*/
void open(int percentage);
/**
* 선루프를 닫습니다.
*/
void close();
/**
* 현재 선루프의 상태를 가져옵니다.
* @return 0: 닫힘, 1: 열림, 2: 틸트
*/
int getStatus();
}
이 AIDL 파일은 빌드 과정에서 자동으로 Java 인터페이스 코드로 변환됩니다. 이 인터페이스는 클라이언트가 서비스를 호출할 때 사용할 프록시(Proxy) 객체와, 서비스 측에서 실제 로직을 구현할 스텁(Stub) 클래스를 포함합니다.
2단계: 서비스 구현
다음으로, 위에서 정의한 AIDL 인터페이스를 구현하는 실제 서비스 코드를 작성합니다. 이 서비스는 안드로이드 프레임워크의 `SystemService` 클래스를 상속받는 것이 일반적입니다. `SystemService`는 시스템 서비스의 생명주기 관리를 위한 편리한 추상 클래스입니다.
packages/apps/MyCarSystem/src/com/example/mycarsystem/MyCarSunroofService.java
package com.example.mycarsystem;
import android.content.Context;
import android.os.Binder;
import android.util.Slog; // 시스템 로그는 Slog를 사용합니다.
import com.android.server.SystemService;
public class MyCarSunroofService extends SystemService {
private static final String TAG = "MyCarSunroofService";
private final Context mContext;
private int mSunroofStatus = 0; // 0: closed, 1: open, 2: tilt
// Binder 구현체
private final IBinder mBinder = new IMyCarSunroofService.Stub() {
@Override
public void open(int percentage) {
Slog.i(TAG, "Opening sunroof to " + percentage + "%");
// 여기에 실제 하드웨어 제어 로직 구현
mSunroofStatus = 1; // 상태 업데이트
}
@Override
public void close() {
Slog.i(TAG, "Closing sunroof.");
// 여기에 실제 하드웨어 제어 로직 구현
mSunroofStatus = 0; // 상태 업데이트
}
@Override
public int getStatus() {
return mSunroofStatus;
}
};
public MyCarSunroofService(Context context) {
super(context);
mContext = context;
}
// SystemService 생명주기 콜백: 부팅 단계에서 호출됨
@Override
public void onStart() {
Slog.i(TAG, "MyCarSunroofService is starting.");
// 서비스를 ServiceManager에 게시하여 다른 프로세스에서 찾을 수 있도록 함
publishBinderService("my_car_sunroof_service", mBinder);
}
// SystemService 생명주기 콜백: 모든 서비스가 시작된 후 호출됨
@Override
public void onBootPhase(int phase) {
super.onBootPhase(phase);
if (phase == SystemService.PHASE_BOOT_COMPLETED) {
Slog.i(TAG, "Boot completed. Sunroof service is ready.");
// 부팅 완료 후 수행할 작업이 있다면 여기에 구현
}
}
}
3단계: AOSP 빌드 시스템 설정 (Android.bp)
이제 작성한 코드를 AOSP 빌드 시스템에 통합해야 합니다. Soong 빌드 시스템에서 사용하는 `Android.bp` 파일을 작성하여 이 모듈이 시스템의 일부로, 특히 프레임워크와 함께 컴파일되도록 설정합니다.
packages/apps/MyCarSystem/Android.bp
{
"//": "MyCarSystem 서비스 모듈 빌드 정의",
"java_library": {
"name": "my-car-system-service",
"srcs": [
"src/**/*.java",
"src/**/*.aidl",
],
// 시스템 앱/서비스는 platform API를 사용해야 함
"platform_apis": true,
"installable": true,
// 이 라이브러리를 services.jar에 포함시킴
"system_ext_specific": true,
}
}
위 설정의 핵심은 다음과 같습니다.
name: 빌드 시스템에서 사용할 모듈의 이름입니다.srcs: 컴파일할 소스 코드와 AIDL 파일의 경로를 지정합니다.platform_apis:@hide로 숨겨진 내부 API에 접근하기 위해 반드시 `true`로 설정해야 합니다.system_ext_specific: 이 모듈이 `system_ext` 파티션에 설치되는 것을 명시합니다. 이를 통해 `services.jar`와 같은 핵심 프레임워크 라이브러리에 우리 코드를 포함시킬 수 있습니다.
4단계: SystemServer에 서비스 등록
이 단계가 바로 마법이 일어나는 곳입니다. 안드로이드 시스템의 모든 서비스가 시작되는 SystemServer.java 파일을 직접 수정하여, 부팅 과정에 우리 서비스를 끼워 넣습니다.
수정할 파일은 frameworks/base/services/java/com/android/server/SystemServer.java 입니다.
이 파일의 startOtherServices() 메소드 내 적절한 위치에 다음 코드를 추가합니다. 이 메소드는 시스템의 주요 서비스들이 시작된 후, 나머지 부가적인 서비스들을 시작하는 역할을 합니다.
// frameworks/base/services/java/com/android/server/SystemServer.java
// ... 기존 import 문들
import com.example.mycarsystem.MyCarSunroofService; // 우리가 만든 서비스 클래스 import
public class SystemServer {
// ...
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
// ... 기존 서비스 시작 코드들
t.traceBegin("StartMyCarSunroofService");
try {
// 시스템 서비스 매니저를 통해 우리 서비스를 시작
mSystemServiceManager.startService(MyCarSunroofService.class);
Slog.i(TAG, "MyCarSunroofService has been started.");
} catch (Throwable e) {
reportWtf("starting MyCarSunroofService", e);
}
t.traceEnd();
// ... 기존 서비스 시작 코드들
}
// ...
}
mSystemServiceManager.startService()를 호출하면, 시스템 서비스 매니저는 해당 클래스의 인스턴스를 생성하고 생명주기 콜백(onStart(), onBootPhase() 등)을 적절한 시점에 호출해줍니다. 이 한 줄의 코드로 우리의 서비스는 안드로이드 프레임워크의 정식 구성원이 됩니다.
5단계: SELinux 정책 추가 (매우 중요)
현대의 안드로이드는 SELinux(Security-Enhanced Linux)를 통해 매우 강력한 접근 제어를 적용합니다. 우리가 만든 서비스가 아무리 SystemServer에서 시작되었더라도, SELinux 정책이 허용하지 않으면 아무것도 할 수 없습니다. 서비스가 필요한 파일, 드라이버, 속성(property) 등에 접근할 수 있도록 허용 규칙을 추가해야 합니다.
SELinux 정책은 복잡한 주제이지만, 가장 기본적인 서비스 등록을 위한 정책은 다음과 같습니다.
device/[vendor]/[product]/sepolicy/system_server.te 파일에 다음을 추가합니다.
# MyCarSunroofService를 위한 정책
# "my_car_sunroof_service" 라는 이름의 서비스를 시스템 서버(system_server) 도메인에 추가하도록 허용
add_service(system_server, my_car_sunroof_service)
만약 서비스가 특정 하드웨어 드라이버(예: /dev/sunroof_driver)에 접근해야 한다면, 해당 드라이버에 대한 타입(type)을 정의하고, system_server가 접근할 수 있도록 허용하는 규칙을 추가해야 합니다.
# device/[vendor]/[product]/sepolicy/file_contexts
/dev/sunroof_driver u:object_r:sunroof_device:s0
# device/[vendor]/[product]/sepolicy/system_server.te
allow system_server sunroof_device:chr_file { read write open ioctl };
SELinux 정책 오류는 logcat 이나 dmesg 로그에 "avc: denied" 메시지로 나타나므로, 서비스가 제대로 동작하지 않을 때는 반드시 이 로그를 확인해야 합니다.
전체 빌드 및 배포
모든 코드 수정이 완료되면, AOSP 소스 코드 전체를 다시 빌드해야 합니다.
source build/envsetup.sh
lunch aosp_car_x86_64-userdebug # 타겟 디바이스에 맞게 선택
m
빌드가 성공적으로 완료되면 생성된 시스템 이미지(system.img, system_ext.img 등)를 타겟 디바이스에 플래싱합니다. 부팅 후 logcat 로그에서 "MyCarSunroofService is starting" 메시지를 확인하거나, adb shell dumpsys my_car_sunroof_service 와 같은 명령어로 서비스 상태를 확인할 수 있습니다.
방법 3: CarService와의 연동 - Automotive 전문 접근 방식
AAOS에는 차량과 관련된 모든 서비스를 총괄하는 중심점, 바로 CarService가 존재합니다. CarPropertyService, CarPowerManagementService 등 차량과 관련된 대부분의 표준 서비스는 이 CarService 내에서 관리됩니다. 따라서 우리가 만드는 서비스가 차량의 속성(Property), 센서, 전원 등 표준화된 차량 기능과 밀접하게 관련이 있다면, 독립적인 시스템 서비스를 만드는 것보다 CarService의 플러그인 형태로 기능을 확장하는 것이 더 효율적이고 표준적인 방법일 수 있습니다.
이 방법은 SystemServer에 직접 등록하는 것보다 덜 침습적이며, AAOS의 아키텍처를 존중하는 방식입니다. 주로 새로운 차량 하드웨어 추상화 계층(VHAL, Vehicle HAL) 속성을 추가하고, 이를 관리하는 로직을 구현할 때 사용됩니다.
구현 흐름
- VHAL 속성 정의: 제어하고자 하는 하드웨어의 속성을 VHAL에 새롭게 정의합니다.
- CarService 내부 서비스 구현:
CarService내부에 새로운 서비스를 담당할 클래스(예:MyCustomFeatureService)를 작성합니다. 이 클래스는 보통ICarService.Stub을 상속받는 다른 서비스들의 구조를 따릅니다. - CarService에 등록:
packages/services/Car/service/src/com/android/car/CarService.java파일의init()메소드에서 다른 서비스들과 마찬가지로 우리 서비스를 생성하고 등록합니다. - API 노출:
CarManager클래스를 통해 앱 개발자들이 사용할 수 있도록 새로운 API를 노출합니다.
이 방식은 AAOS 프레임워크 자체에 대한 깊은 이해를 요구하며, 주로 OEM이나 Tier-1 공급업체에서 플랫폼을 커스터마이징할 때 사용하는 고급 기법입니다. 단순한 부팅 시점 실행을 넘어, AAOS 생태계에 완전히 통합되는 서비스를 만들고자 할 때 고려해볼 수 있는 최상의 접근법입니다.
각 방법 비교 및 최적의 선택
지금까지 살펴본 세 가지 방법을 각각의 특징에 따라 비교해 보면, 어떤 상황에 어떤 방법을 선택해야 할지 명확해집니다.
| 항목 | 방법 1: BOOT_COMPLETED | 방법 2: SystemServer 등록 | 방법 3: CarService 연동 |
|---|---|---|---|
| 시작 시점 | 느림 (부팅 완료 후) | 매우 빠름 (시스템 부팅 초기) | 빠름 (CarService 초기화 시점) |
| 구현 난이도 | 매우 쉬움 | 어려움 (AOSP 수정 필요) | 매우 어려움 (AAOS 프레임워크 이해 필요) |
| 안정성 | 낮음 (시스템에 의해 종료될 수 있음) | 매우 높음 (시스템 프로세스) | 매우 높음 (핵심 서비스의 일부) |
| 필요 권한 | 일반 앱 권한 | 시스템/시그니처 권한 | 시스템/시그니처 권한 |
| 시스템 접근성 | 제한적 | 최상 (내부 API, 하드웨어 직접 접근) | 높음 (차량 관련 API 및 하드웨어) |
| 주요 사용 사례 | 사용자 편의 기능, 부팅 후 데이터 동기화 등 비핵심 기능 | 독자적인 하드웨어 제어, 커스텀 보안 솔루션, 차량 핵심 로직 구현 | 표준 차량 기능 확장 (새 센서, 액추에이터 추가), VHAL 연동 기능 개발 |
마치며: 안정적인 서비스는 신뢰의 초석
Android Automotive OS에서 부팅 시 서비스를 자동으로 실행하는 것은 단순히 기능을 구현하는 것을 넘어, 차량 시스템 전체의 안정성과 신뢰성을 설계하는 과정입니다. 사용자는 시동을 걸었을 때 모든 기능이 즉각적이고 매끄럽게 동작하기를 기대합니다. 우리가 만든 서비스가 부팅 과정의 어느 시점에, 어떤 권한을 가지고, 어떻게 실행되는지를 완벽하게 제어할 수 있을 때, 비로소 그 기대를 충족시킬 수 있습니다.
BOOT_COMPLETED와 같은 편리한 길도 있지만, 때로는 AOSP 소스 코드의 깊숙한 곳으로 들어가 시스템의 심장을 직접 만져야만 해결할 수 있는 문제들이 있습니다. SystemServer에 서비스를 등록하는 과정은 분명 복잡하고 어렵지만, 이 과정을 통해 얻게 되는 시스템에 대한 깊은 이해와 제어 능력은 여러분을 더 뛰어난 AAOS 개발자로 만들어 줄 것입니다.
오늘 다룬 내용들이 여러분의 다음 AAOS 프로젝트에서 마주할 문제를 해결하는 데 든든한 기반이 되기를 바랍니다. 안정적인 시스템 서비스는 결국 운전자와 탑승자의 안전하고 즐거운 주행 경험으로 이어진다는 사실을 기억하십시오.
Post a Comment