안드로이드 기반의 소스코드를 직접 수정하여 스마트 단말이나 커스텀 가전, 전장 하드웨어를 제어하다 보면 반드시 마주하게 되는 작업이 있습니다. 바로 우리만의 자바 기반 '시스템 서비스(System Service)'를 운영체제 심장부에 정식으로 입격시키는 일이죠.
안드로이드의 심장부인 SystemServer 프로세스는 부팅 시 시스템에 필요한 수많은 핵심 서비스들을 차례대로 로드하여 메모리에 적재합니다. 이때 단순한 자바 클래스 형태로 객체를 던져주느냐, 혹은 구글의 표준 라이프사이클 프레임워크인 SystemService 구조를 채택하느냐에 따라 단말의 부팅 안정성과 전력 관리 효율이 완전히 달라집니다. 이번 포스팅에서는 최신 AOSP 프레임워크 아키텍처 기준에 맞춘 시스템 서비스 빌드 패턴을 살펴보고, SystemServer.java 소스를 안전하게 수정하여 클라이언트와 바인더 IPC 링크를 매끄럽게 연결하는 전 과정을 상세히 공유해 드리겠습니다.

📌 핵심 요약 3줄
- 현대적인 안드로이드 시스템 서비스는 com.android.server.SystemService 표준 클래스를 상속받아 OS 부팅 수명 주기 매니저의 통제를 받도록 구현해야 합니다.
- SystemServer.java에 서비스를 심을 때 구형 유산 방식인 addService()를 남용하면 라이프사이클 관리가 깨지므로, mSystemServiceManager.startService()를 쓰는 것이 정석입니다.
- 클라이언트는 ServiceManager.getService()를 통해 리눅스 커널의 로우 바인더를 인출한 뒤, AIDL Stub 객체를 통해 프로세스 장벽을 넘어 원격 함수를 호출합니다.
1. 안드로이드 시스템 서비스 등록 메커니즘 아키텍처 비교
SystemServer 내부에서 서비스를 구동할 때 엔지니어가 선택할 수 있는 두 가지 등록 패러다임의 명확한 차이점입니다.
| 아키텍처 비교 항목 | 최신 프레임워크 표준 방식 (startService) | 구형 레거시 유산 방식 (addService) |
| 기반 상속 클래스 | com.android.server.SystemService 코어 확장 | 일반 IBinder 또는 AIDL Stub 객체 직렬화 |
| SystemServer 등록 코드 | mSystemServiceManager.startService(클래스.class); | ServiceManager.addService("이름", 바인더인스턴스); |
| OS 수명 주기 연동 여부 | 부팅 단계별(onBootPhase) 세부 제어 및 유저 스위칭 이벤트 수신 가능 | 단순 바인더 허브 등록에 그치며, OS 라이프사이클 이벤트를 직접 받지 못함 |
| 바인더 퍼블리시 주체 | 서비스 내부 onStart() 런타임 시점에서 자율적으로 수행 | SystemServer 메인 부팅 스레드가 외부에서 강제로 밀어 넣음 |
| 권장되는 유스케이스 | 양산형 펌웨어 개발 시 새로 추가되는 모든 핵심 도메인 서비스 | 하부 C++ 데몬과 1:1 매핑되는 독립형 경량 자바 프록시 서비스 |
2. 표준 SystemService 패턴 기반의 서비스 구현
구글의 프레임워크 표준 매니저 규격을 준수하여, 부팅 시 충돌 없이 안전하게 메모리에 안착하는 커스텀 시스템 서비스 소스코드입니다.
2.1 AIDL 원격 통신 인터페이스 정의
서로 다른 프로세스 영역에 상주하는 클라이언트 앱과 시스템 서비스가 메시지를 주고받을 수 있도록 IPC 포맷을 선언합니다.
// frameworks/base/core/java/com/example/systemservice/IMySystemService.aidl
package com.example.systemservice;
interface IMySystemService {
String getMessage();
}
2.2 코어 시스템 서비스 클래스 구현
안드로이드 SystemService 수명 주기 래퍼를 상속받아 구현하되, 내부에서 실제 바인더 Stub을 생성하여 매핑하는 구조가 가장 깔끔합니다.
// frameworks/base/services/core/java/com/android/server/custom/MySystemService.java
package com.android.server.custom;
import android.content.Context;
import android.os.IBinder;
import android.os.IMySystemService; // AIDL 컴파일 엔진이 빌드해 낸 자바 클래스
import android.os.RemoteException;
import android.util.Slog;
import com.android.server.SystemService;
public class MySystemService extends SystemService {
private static final String TAG = "MySystemServiceCore";
// 1. 실제 IPC 트랜잭션을 처리해 줄 바인더 Stub 객체를 내부에 캡슐화합니다.
private final IMySystemService.Stub mBinderImpl = new IMySystemService.Stub() {
@Override
public String getMessage() throws RemoteException {
Slog.d(TAG, "클라이언트 앱의 바인더 원격 트랜잭션 수신 완료 - 메시지 반환 처리");
return "Hello from MySystemService! 완벽하게 등록된 안드로이드 코어 서비스입니다.";
}
};
public MySystemService(Context context) {
super(context);
}
@Override
public void onStart() {
Slog.i(TAG, "MySystemService 런타임 온스타트 단계 진입");
// 2. 부팅 매니저가 내 서비스를 켤 때, 커널의 ServiceManager 바인더 레지스트리에 내 이름표를 정식 등록합니다.
publishBinderService("my_system_service_custom", mBinderImpl);
}
}
3. SystemServer.java 수정을 통한 시스템 부팅 시퀀스 적재
작성된 서비스를 시스템이 켜질 때 자동으로 로드하도록 자바 프레임워크 부팅 코어 엔진 소스를 수정합니다.
// frameworks/base/services/java/com/android/server/SystemServer.java
import com.android.server.custom.MySystemService; // 내 커스텀 서비스 패키지 임포트
public final class SystemServer {
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
// ... 안드로이드 순정 다른 코어 서비스들 가동 중 ...
t.traceBegin("StartMyCustomSystemService");
try {
Slog.i(TAG, "커스텀 하드웨어 제어 시스템 서비스 인스턴스화 시작");
// [주의] 수명 주기 추상 클래스를 상속받았으므로 addService가 아닌 startService로 대행합니다.
// 이렇게 하면 SystemServiceManager가 내부적으로 생성자를 호출하고 onStart()까지 순차 가동해 줍니다.
mSystemServiceManager.startService(MySystemService.class);
} catch (Throwable e) {
Slog.e(TAG, "MySystemService 구동 시퀀스 도중 치명적 에러 발생", e);
}
t.traceEnd();
// ... 기존 부팅 시퀀스 계속 진행 ...
}
}
4. 클라이언트(애플리케이션 레이어)에서 바인더 링킹 후 호출 방법
운영체제 시스템 내부에 서비스가 상주했다면 이제 일반 클라이언트 앱 컴포넌트(예: Activity, Service) 단에서 바인더 주소록을 뒤져 원격 함수를 당겨옵니다.
package com.example.client;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager; // 일반 서드파티 앱 빌드 시에는 숨겨져 있으므로 플랫폼 SDK 빌드 환경 필요
import com.example.systemservice.IMySystemService;
import android.util.Log;
public class MySystemServiceClient {
private static final String TAG = "ClientSystemLink";
public void connectAndCallService() {
// 1. 전역 서비스매니저에 등록된 로우 레벨 리눅스 커널 바인더 객체를 스트레이트로 들고 옵니다.
IBinder binder = ServiceManager.getService("my_system_service_custom");
if (binder == null) {
Log.e(TAG, "시스템 서비스의 바인더 핸들을 획득하는 데 실패했습니다. 등록 상태를 확인하세요.");
return;
}
// 2. 가져온 로우 바인더를 AIDL 명세서 규격에 맞는 프록시 객체 인터페이스로 한 단계 래핑합니다.
IMySystemService service = IMySystemService.Stub.asInterface(binder);
try {
// 3. 프로세스 샌드박스 경계를 넘어 SystemServer의 메모리 영역에 있는 함수를 원격 실행합니다.
String systemResponse = service.getMessage();
Log.i(TAG, "시스템 서버로부터 안전하게 복원된 문자열 데이터: " + systemResponse);
} catch (RemoteException e) {
Log.e(TAG, "바인더 IPC 통신 중 원격 트랜잭션 예외가 발생했습니다.", e);
}
}
}
🛠️ 개발을 위한 팁 (Tips)
- onBootPhase() 오버라이딩으로 정교한 타이밍 제어: 내가 만든 커스텀 서비스가 가동될 때 내부적으로 다른 순정 서비스(예: PackageManagerService나 PowerManagerService)를 참조하여 초기화 연동을 해야 한다면, 단순히 onStart() 안에서 코드를 실행하면 안 됩니다. 그 시점에는 타 서비스들이 아직 메모리에 안 떴을 확률이 높기 때문이죠. 이때 클래스 내부에 public void onBootPhase(int phase) 메서드를 구현하고 PHASE_SYSTEM_SERVICES_READY 또는 PHASE_BOOT_COMPLETED 조건 분기를 걸어두면 다른 코어 서비스들이 완벽히 정착한 타이밍을 안전하게 캐치하여 링킹할 수 있습니다.
- adb shell service list 명령어로 등록 즉시 교차 검증: SystemServer.java 수정 후 펌웨어 빌드를 마쳤다면 단말을 켜고 터미널 창에 adb shell service list | grep my_system_service 명령을 입력해 보세요. 내 서비스가 커널 레지스트리에 문자열 태그와 함께 정상 등록되어 작동 중인지, 아니면 부팅 도중 크래시로 누락되었는지 1초 만에 스캔할 수 있어 삽질 시간을 획득하는 최고의 지름길이 됩니다.
⚠️ 흔히 하는 실수 (Common Mistakes)
- SystemService 확장 객체를 구형 addService()로 강제 매핑: 자바 코딩 관점에서 클래스 형식을 혼동하여 발생하는 흔한 설계 에러입니다. class MySystemService extends SystemService 구조로 만들어진 객체 인스턴스를 SystemServer.java 소스 안에서 ServiceManager.addService("이름", new MySystemService(context)) 형태로 직접 집어넣으려고 시도하는 실수입니다. addService() 함수는 로우 레벨의 IBinder 인터페이스 타입만 인자로 받아들입니다. SystemService는 바인더 객체가 아니라 안드로이드의 고수준 수명 주기 래퍼 클래스이므로, 이 객체를 통째로 던지면 즉시 컴파일 에러가 나거나 실행 시 클래스 형변환 예외로 부팅 무한 루프 크래시를 유발합니다.
- 원격 바인더 리턴 객체의 널(Null) 예외 처리 누락: 시스템 서비스 호출 앱을 만들 때 ServiceManager.getService()가 반환한 binder 객체가 당연히 항상 살아있을 것이라 착각하고 asInterface(binder)를 곧바로 난타하는 실수입니다. 시스템 서버가 부팅 직후 극도로 바쁜 타이밍이거나, 서비스 코어 단에 크래시가 발생해 서비스가 일시적으로 내려간 상황이라면 getService()는 여차없이 null을 리턴합니다. 이에 대한 널 체크 가드 코드가 없으면 클라이언트 프로세스까지 런타임 NullPointerException으로 동반 사망하게 되니 방어적 코딩을 필수적으로 심어야 합니다.
5. 결론
안드로이드 코어 프레임워크의 자바 생태계에 정식으로 커스텀 시스템 서비스를 빌트인하는 과정은 플랫폼 전체의 생명 주기를 제어하는 마스터 개발자로 거듭나기 위한 최고의 관문입니다.
기존의 단순 바인더 매핑 방식인 구형 유산 패턴을 탈피하여 안드로이드 표준 아키텍처에 부합하는 SystemService 클래스 기반의 수명 주기 등록 체계를 갖추어야만, 향후 단말 OS 버전 업그레이드나 전력 효율 최적화 단계에서 코드가 꼬이지 않는 튼튼한 인프라를 유지할 수 있습니다. 펌웨어 이미지를 플래싱한 뒤 로그캣 상에 서비스 초기화 크래시 메시지가 끝없이 쏟아지거나, 클라이언트 프로세스에서 바인더 핸들을 불러올 때 권한 거부(SecurityException) 에러를 마주해 벽에 부딪히셨다면 언제든 하단 댓글란에 에러 스택을 남겨주세요. 하부 커널 통로와 자바 레이어의 연결 고리를 매끄럽게 교정해 드리겠습니다!
'Android System & AOSP Engineering > AOSP Framework & Custom Services' 카테고리의 다른 글
| AOSP 실무: frameworks/base 내부 커스텀 시스템 서비스 추가 및 Android.bp 빌드 가이드 (0) | 2025.06.08 |
|---|---|
| AOSP 실무: Native Binder IPC 구현 및 Android.bp 기반 C++ 데몬 서비스 등록 가이드 (0) | 2025.06.07 |
| Custom System Service 설계: AIDL IPC 선택 기준과 Thread-Safe 아키텍처 구현 (0) | 2025.06.05 |
| AOSP 개발 실무: SystemServer 서비스 아키텍처와 Android.bp 기반 커스텀 구현 (0) | 2025.06.04 |
| AOSP 입문: Custom System Service 개념부터 AIDL 바인더 등록까지 완벽 정리 (0) | 2025.06.03 |