Android System & AOSP Engineering/AOSP Framework & Custom Services

AOSP 실무: 커스텀 시스템 서비스 앱 연동 및 OTA 업데이트 분기 시 소스 영속화 가이드

임베디드 친구 2025. 6. 12. 22:09
반응형

나만의 커스텀 시스템 서비스를 만들고 HAL 레이어와의 통신 채널까지 튼튼하게 구축했다면, 이제 이 아키텍처의 종착역인 '상위 애플리케이션과의 연동'과 '상용 펌웨어 배포 및 무선 업데이트(OTA)'라는 거대한 장벽을 넘어야 합니다. 아무리 내부 서비스를 잘 짜놓았어도 일반 안드로이드 앱에서 이 서비스를 편하게 호출할 수 없다면 무용지물이며, 단말기가 OTA 업데이트를 받은 직후 내가 심어둔 커스텀 코드가 통째로 날아가 버린다면 양산형 기기로서 자격을 상실하기 때문입니다.

특히 안드로이드 10 이후의 AOSP 환경은 구글의 프로젝트 트레블(Project Treble) 정책으로 인해 시스템 파티션과 벤더 파티션의 국경선이 매우 삼엄해졌습니다. 자바 기반의 시스템 서버 인프라를 함부로 변형하면 부팅 시 보안 모듈에 의해 거부당하기 십상이죠. 오늘은 내가 공들여 만든 커스텀 시스템 서비스를 안전하게 상위 앱 레이어로 노출하는 정석적인 바인딩 기법과, 펌웨어 전역 업데이트 과정에서도 소스코드가 유실되지 않고 무결성을 유지하는 실전 배포 파이프라인을 자세히 공유해 드리겠습니다.

Generated by Gemini AI.

📌 핵심 요약 3줄

  1. 일반 앱 레이어는 비공개 숨김 클래스인 ServiceManager에 직접 접근할 수 없으므로, AIDL 바인더 객체를 인출하여 인터페이스로 형변환하는 명확한 클라이언트 래퍼 메커니즘을 적용해야 합니다.
  2. 자바 기반의 시스템 서비스는 system_server 프로세스 힙 내부에서 함께 생멸하므로 독립 네이티브 데몬처럼 init.rc로 관리하는 것이 아니라, 프레임워크 컴파일 트리에 고착시켜야 합니다.
  3. OTA 업데이트 시 커스텀 소스의 유실을 막으려면 frameworks/base를 직접 수정하는 생짜 코딩을 지양하고, 내 타깃 기기 전용 보드 디렉터리(device/제조사/보드명) 내부에 패치 가드레일을 쳐두어야 합니다.

1. 시스템 서비스 빌드, 연동 및 배포 라이프사이클 비교

커스텀 서비스가 전체 OS 인프라 내부에서 유기적으로 빌드되고 배치되는 전 과정에 대한 아키텍처 대조표입니다.

프로세스 관리 단계 핵심 제어 파일 및 경로 예시 연동 및 처리 메커니즘 실무 엔지니어 핵심 체크포인트
1. AIDL 컴파일 단계 frameworks/base/core/java/ 내 .aidl 파일 Android.bp에 소스 등록 후 빌드 타임에 Java Stub 클래스 자동 자동 생성 컴파일 우선순위 및 빌드 네임스페이스 충돌 방지
2. 런타임 등록 단계 SystemServer.java 소스 코드 수정 구성 ServiceManager.addService() 호출을 통해 바인더 주소록에 전역 인스턴스 영구 게시 부팅 페이즈 타이밍 분리를 통한 데드락 유발 방지
3. 앱 클라이언트 연동 제조사 전용 SDK 빌드 또는 시스템 앱 배치 IBinder 객체를 확보한 후 ICustomLoggerService.Stub.asInterface()로 다운캐스팅 진행 @hide API 장벽 우회 또는 단말 서명 권한 획득 여부 체크
4. OTA 배포 및 유지 device/vendor_name/board_name/ 패치 트리 시스템 파티션 이미지 패키징 시 커스텀 서비스 모듈 강제 결합 및 유실 차단 구글 순정 소스 업데이트 시 코드 충돌(Merge Conflict) 방지 전략 구축

2. 커스텀 시스템 서비스 구현 및 앱 레이어 연동 뼈대 코드

2.1 AIDL 인터페이스 정의와 시스템 서버 가동 코드

안드로이드 프로세스 간 경계를 뛰어넘어 데이터를 주고받기 위한 통신 규격을 명세합니다.

Java
 
// frameworks/base/core/java/android/os/ICustomLoggerService.aidl
package android.os;

/** {@hide} */ // 일반 서드파티 앱 개발자들에게 함부로 노출되지 않도록 가이드 명시
interface ICustomLoggerService {
    void logMessage(String message);
}

위 규격을 기반으로 실제 메모리상에서 비즈니스 로직을 처리할 시스템 서비스 본체를 구현하고 서버에 올립니다.

Java
 
// frameworks/base/services/core/java/com/android/server/CustomLoggerService.java
package com.android.server;

import android.content.Context;
import android.os.ICustomLoggerService;
import android.os.RemoteException;
import android.util.Slog;

public class CustomLoggerService extends ICustomLoggerService.Stub {
    private static final String TAG = "CustomLoggerService";
    private final Context mContext;

    public CustomLoggerService(Context context) {
        this.mContext = context;
    }

    @Override
    public void logMessage(String message) throws RemoteException {
        // 전역 시스템 서버 로그 풀에 안전하게 기록합니다.
        Slog.d(TAG, "[중앙 제어 로그 수집] " + message);
    }
}

이제 시스템이 부팅될 때 이 서비스 인스턴스를 주소록에 등록해 줍니다.

Java
 
// frameworks/base/services/java/com/android/server/SystemServer.java
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
    // ... 기존 순정 서비스 가동 코드들 ...
    
    t.traceBegin("StartCustomLoggerService");
    try {
        Slog.i(TAG, "사용자 정의 CustomLoggerService를 전역 바인더 주소록에 등록합니다.");
        ServiceManager.addService("custom_logger", new CustomLoggerService(mSystemContext));
    } catch (Throwable e) {
        reportWtf("CustomLoggerService 가동 실패", e);
    }
    t.traceEnd();
}

2.2 클라이언트 앱 진영에서의 바인더 바인딩 제어 코드

일반 서드파티 앱 환경에서는 ServiceManager 클래스가 숨겨져 있으므로, 플랫폼 서명을 받은 시스템 권한 앱이거나 제조사 특화 관리 앱 환경에서 리플렉션을 쓰거나 커스텀 SDK 프레임워크 자원을 링크하여 아래 방식으로 바인더 채널을 인출해야 합니다.

Java
 
package com.example.adminapp;

import android.os.IBinder;
import android.os.RemoteException;
import android.os.ICustomLoggerService;
import android.util.Log;
import java.lang.reflect.Method;

public class LoggerClientManager {
    private static final String TAG = "LoggerClientManager";
    private ICustomLoggerService mService;

    public LoggerClientManager() {
        try {
            // 히든 API인 ServiceManager를 리플렉션 테크닉으로 안전하게 우회하여 바인더 자원을 획득합니다.
            Class<?> serviceManagerClass = Class.forName("android.os.ServiceManager");
            Method getServiceMethod = serviceManagerClass.getMethod("getService", String.class);
            IBinder binder = (IBinder) getServiceMethod.invoke(null, "custom_logger");

            if (binder != null) {
                // AIDL 인터페이스 프록시 객체로 캐스팅 완료
                mService = ICustomLoggerService.Stub.asInterface(binder);
                Log.i(TAG, "CustomLoggerService 시스템 바인더 채널 개통 성공!");
            } else {
                Log.e(TAG, "시스템 서비스 바인더 자원을 찾을 수 없습니다.");
            }
        } catch (Exception e) {
            Log.e(TAG, "리플렉션 우회 중 에러 발생: " + e.getMessage());
        }
    }

    public void sendSystemLog(String msg) {
        if (mService != null) {
            try {
                mService.logMessage(msg);
            } catch (RemoteException e) {
                Log.e(TAG, "IPC 호출 도중 크래시가 발생했습니다.", e);
            }
        }
    }
}

3. OTA updates 배포 시 커스텀 서비스 무결성 유지 공정

구글 순정 안드로이드 소스가 업데이트되어 전체 뼈대 파일들이 덮어써 지더라도 내가 추가한 기능이 유실되지 않도록 아키텍처를 영속화하는 정석 가이드라인입니다.

3.1 frameworks/base 생짜 수정 자제 및 device/ 패치 트리 분리

AOSP 코어 소스 코드를 무작위로 직접 칼질해 두면, 추후 구글이 배포하는 보안 패치나 OS 판올림 진영의 코드를 병합(Merge)할 때 수백 개의 코드 충돌이 발생하여 빌드가 터지게 됩니다.

따라서 내 커스텀 코드는 독립된 컴파일 모듈 파일로 격리해 두고, 타깃 보드 디렉터리 내부의 소스 제어 파일들을 활용해 컴파일 라인에 우회 등록해야 합니다.

Plaintext
 
AOSP_SOURCE_ROOT/
  └── device/
      └── 제조사명(vendor_example)/
          └── 보드명(custom_board)/
              ├── board_manifest.xml  (SELinux 정책 및 서비스 명세 보존 공간)
              └── CustomLoggerOverlay/ (순정 자원을 건드리지 않고 환경 설정을 주입하는 곳)

3.2 안전한 SELinux 보안 정책 권한 명세 자형 주입

안드로이드 10 이상의 보안 가드레일은 SELinux 정책에 등록되지 않은 임의의 서비스를 시스템 바인더 주소록에 올리는 행위를 철저히 차단합니다. 정책을 누락하면 런타임에 서비스가 강제 차단됩니다.

코드 스니펫
 
# device/vendor_example/custom_board/sepolicy/service.te
# 내 사용자 정의 서비스를 시스템 서버가 관리하는 정식 서비스 유형으로 도메인 선언
type custom_logger_service, service_manager_type;
코드 스니펫
 
# device/vendor_example/custom_board/sepolicy/service_contexts
# 바인더 주소록 문자열 맵과 내 SELinux 도메인을 정확하게 1:1 매핑 선언
custom_logger                         u:object_r:custom_logger_service:s0
코드 스니펫
 
# device/vendor_example/custom_board/sepolicy/system_server.te
# 시스템 서버 프로세스가 내 서비스를 주소록에 등록(add)하고 찾을(find) 수 있도록 전권 부여
allow system_server custom_logger_service:service_manager { add find };

🛠️ 개발을 위한 팁 (Tips)

  1. Context.getSystemService() 표준 매핑 우회 확장: 상용 제품군을 완성도 높게 빌드하고 싶다면 클라이언트 앱에서 매번 위의 리플렉션 코드를 치게 만들지 마세요. frameworks/base/core/java/android/app/ContextImpl.java 소스 파일을 열어 시스템 서비스 레지스트리 스태틱 블록(SystemServiceRegistry.java) 내부에 내 서비스를 등록해 두면, 클라이언트 앱 진영에서 순정 서비스들과 완벽히 대등하게 getContext().getSystemService("custom_logger") 라는 한 줄의 표준 코드로 깔끔하게 내 서비스를 인출해 쓸 수 있게 됩니다.
  2. vintf_fragments 명세 활용: AIDL 서비스를 독립 보드 트리에 안착시킬 때는 하드웨어 컴파일 산출물 매니페스트에 국한하지 말고, 해당 모듈 디렉터리 내부에 독립된 소형 .xml 파편 파일을 둔 뒤 Android.bp 빌드 스크립트에 vintf_fragments: ["my_manifest.xml"] 형태로 결합해 주세요. 이렇게 하면 구글 순정 뼈대 파일을 단 한 줄도 건드리지 않고 완벽하게 내 커스텀 소스 영역만 떼어내어 깔끔하게 통합 배포할 수 있습니다.

⚠️ 흔히 하는 실수 (Common Mistakes)

  1. 자바 기반 서비스를 네이티브 데몬인 것처럼 init.rc에 등록하는 행위: 가장 흔하게 범하는 개념상의 아키텍처 충돌 실수입니다. 이번 가이드에서 구현한 CustomLoggerService는 자바 가상머신 위에서 작동하며 system_server라는 거대한 하우스 프로세스 안의 한 가닥 스레드로 상주하는 객체입니다. 리눅스 시스템 레벨의 독립 실행 파일(Executable Binary)이 아니므로, 리눅스 초기화 스크립트인 init.rc에 service custom_logger /system/bin/... 형태로 등록해 봤자 해당 경로에 실행 파일이 존재하지 않아 부팅 시 에러 로그만 무수히 뿜어내며 시스템 자원만 갉아먹게 되니 주의하세요.
  2. 클라이언트 앱 단에서 uses-permission 오설정: 사용자 정의 서비스를 보호하겠다는 취지로 시스템 서비스 소스 코드 내부에서 앱의 권한을 체크하도록 설계해 놓고, 정작 호출 주체인 클라이언트 앱의 AndroidManifest.xml에 권한 명세를 누락하거나, 플랫폼 서명 키(Platform Signature Key)로 앱을 사이닝하지 않은 채 배포하는 실수입니다. 시스템 바인더 자원에 접근하는 권한인 BIND_SYSTEM_SERVICE 같은 등급은 일반 앱이 선언해 봤자 구글 플레이스토어 배포 버전에서는 무조건 거부당하므로, 반드시 제조사 서명 키로 앱을 래핑하여 타깃 시스템 가드레일을 통과시켜야 정상 연동됩니다.

4. 결론

사용자 정의 안드로이드 시스템 서비스를 완벽하게 마스터하여 상용 제품에 안착시키는 최종 관문은, 독립된 컴파일 격리 환경을 구축하여 플랫폼의 무결성을 지켜내는 배포 가인드라인 수립에 있습니다.

아키텍처 족보를 무시하고 frameworks/base 코어 소스를 날것으로 도려내거나 숨겨진 API를 무방비하게 앱에 노출하면, 구글의 월간 보안 패치 병합 주기가 돌아올 때마다 거대한 소스 충돌과 런타임 서명 거부라는 부메랑을 맞아 프로젝트 전체가 깊은 수렁에 빠지게 됩니다. 타깃 장치 특화 device/ 폴더 하부에 SEL인증 정책을 정교하게 도려내어 격리 이식하고, 바인더 캐스팅 통로를 표준 매니저 형태로 정석 래핑해야만 수년 간의 OTA 업데이트 주기 속에서도 흔들림 없이 동작하는 명품 커스텀 OS가 완성됩니다.

앱 연동 과정에서 바인더 인터페이스 캐스팅 타이밍이 맞지 않아 Null 객체가 반환되거나, OTA 이미지 패키징 중 SELinux Policy 컴파일 실패 덤프를 만나 밤을 지새우고 계신다면 지체 없이 하단 댓글 창에 빌드 에러 조각을 공유해 주세요. 견고한 안드로이드 트레블 아키텍처의 해법을 함께 짚어드리겠습니다!

반응형