AIDL API 가이드라인

여기에 안내된 권장사항은 특히 안정적이고 하위 호환되는 API를 정의하는 데 AIDL을 사용하는 경우 인터페이스의 유연성에 중점을 두고 AIDL 인터페이스를 효율적으로 개발하기 위한 가이드로 기능합니다.

AIDL은 앱이 백그라운드 프로세스에서 다른 앱과 인터페이스하거나 시스템과 인터페이스해야 하는 경우에 API를 정의하는 데 사용할 수 있습니다.

@VintfStability가 적용된 안정적인 AIDL은 HAL 인터페이스에 사용되며 클라이언트와 서버가 독립적으로 업데이트될 수 있습니다. 이를 위해서는 하위 호환성과 구조화된 데이터가 필요합니다.

AIDL을 사용하여 앱의 프로그래밍 인터페이스를 개발하는 방법에 관한 자세한 내용은 Android 인터페이스 정의 언어(AIDL)를 참고하세요. 실제로 사용되는 AIDL의 예시를 보려면 HAL용 AIDL안정적 AIDL을 참고하세요.

버전 관리

AIDL API의 모든 하위 호환 스냅샷에는 대응되는 버전이 있습니다. 스냅샷을 작성하려면 m <module-name>-freeze-api를 실행합니다. API의 클라이언트 또는 서버가 릴리스될 때마다 (예: Mainline 트레인에서) 스냅샷을 찍고 새 버전을 만들어야 합니다. 시스템-벤더 API의 경우 연례 플랫폼 개정 과정에서 이 작업을 진행해야 합니다.

인터페이스가 고정되면 (버전이 관리되는 aidl_api 디렉터리에 저장됨) 수정해서는 안 됩니다. current 디렉터리만 수정할 수 있습니다. 인터페이스 끝에 메서드를 추가하고, parcelable 끝에 필드를 추가하고, enum에 열거자를 추가하고, union에 멤버를 추가해도 안전합니다.

이전 서버에서 새 메서드를 호출하는 클라이언트는 UNKNOWN_TRANSACTION 오류를 수신하며, 이 오류는 클라이언트에서 적절하게 처리해야 합니다.

허용되는 변경사항에 관한 자세한 내용은 버전 관리 인터페이스를 참고하세요.

빌드 종속 항목

Android 모듈은 aidl_interface에서 생성된 라이브러리의 여러 버전에 종속될 수 없습니다. 라이브러리의 여러 버전이 동일한 네임스페이스에서 동일한 유형을 정의합니다. Android의 aidl 빌드 시스템은 이 문제를 식별하고 라이브러리의 버전이 일치하지 않는 종속 항목 그래프마다 오류를 발생시킵니다.

이로 인해 모듈에 자체 종속 항목이 있는 종속 항목이 많이 포함된 경우 공통 인터페이스의 한 버전을 업데이트하기 어려울 수 있습니다.

개발자는 aidl_interface_defaults를 사용하여 공유 인터페이스의 다른 인터페이스에 대한 종속 항목을 선언할 수 있으므로 모두 독립적으로 업데이트할 필요가 없습니다.

생성된 라이브러리의 종속 항목을 정리하려면 *_defaults 모듈 (예: rust_defaults, cc_defaults, java_defaults)을 사용하는 것이 좋습니다. 인터페이스의 latest 버전의 기본값과 이전 버전의 기본값이 아직 사용 중인 경우 이전 버전의 기본값을 갖는 것이 일반적입니다.

개발자는 aidl_interface_defaults를 사용하여 공유 인터페이스의 다른 인터페이스에 대한 종속 항목을 선언할 수 있으므로 모두 독립적으로 업데이트할 필요가 없습니다.

API 디자인 가이드라인

일반

1. 모든 것 문서화

  • 모든 메서드의 시맨틱, 인수, 내장 예외 사용 방식, 서비스별 예외, 반환 값을 문서화합니다.
  • 모든 인터페이스의 시맨틱을 문서화합니다.
  • enum 및 상수의 시맨틱 의미를 문서화합니다.
  • 구현자가 명확하게 파악하기 어려운 내용이 있다면 모두 문서화합니다.
  • 도움이 될 만한 예제를 제공합니다.

2 표기법

형식에는 대문자 카멜 표기법을, 메서드, 필드, 인수에는 소문자 카멜 표기법을 사용합니다. 예를 들어, parcelable 형식의 경우 MyParcelable을, 인수의 경우 anArgument를 사용합니다. 약어 단어에는 약어 표기법을 사용합니다(NFC -> Nfc).

[-Wconst-name] enum 값과 상수는 각각 ENUM_VALUECONSTANT_NAME으로 사용합니다.

3. 전역 지식이 필요하지 않음

API는 개발자가 전체 코드베이스에 대한 전반적인 지식이나 특정 도메인 전문 지식이 있다고 가정해서는 안 됩니다. 도메인별 식별자 (예: 기기 이름, ID, 핸들)를 처리할 때는 다음을 따르세요.

  • 인터페이스의 양쪽에서 모두 알아야 하는 경우 이러한 식별자가 어디에서 왔는지와 형식을 명시적으로 문서화하세요.
  • 또는 인터페이스별 식별자 (예: 바인더 객체 또는 맞춤 토큰)를 사용하고 한쪽에서 기본 값에 대한 매핑을 관리하도록 합니다. 이렇게 하면 충돌이 줄어들고 사용자가 자신의 영역 외부의 구현 세부정보를 이해하지 않아도 됩니다.

4. 모든 데이터는 구조화되어 있으며 이전 버전과 호환됩니다.

string, byte[], 공유 메모리와 같은 비구조화 데이터는 콘텐츠의 형식이 안정적이거나 인터페이스의 한쪽에서 불투명해야 합니다.

예를 들어 결과의 오류 메시지로 사용되는 문자열 인수는 디버깅을 위해 수신되고 로깅될 수 있지만 형식과 콘텐츠가 이전 버전과 호환되지 않을 수 있으므로 파싱되고 해석되어서는 안 됩니다. 인터페이스의 다른 쪽에서 런타임에 오류가 무엇인지 알아야 하는 경우 enum, 상수 또는 ServiceSpecificException를 사용합니다.

마찬가지로 안정적이고 하위 호환되지 않는 한 객체를 byte[] 또는 공유 메모리로 직렬화하지 마세요. 경우에 따라 @FixedSize 주석을 사용하여 공유 메모리 및 빠른 메시지 대기열에서 parcelable 및 union을 공유할 수 있습니다.

인터페이스

1. 이름 지정

[-Winterface-name] 인터페이스 이름은 I로 시작해야 합니다(예: IFoo).

2 id 기반 '객체'를 갖는 대규모 인터페이스 사용 지양

특정 API와 관련된 호출이 여러 개인 경우에는 대규모 인터페이스가 아닌 하위 인터페이스를 사용합니다. 이러한 접근 방식은 다음과 같은 이점을 제공합니다.

  • 클라이언트 또는 서버 코드를 더 쉽게 이해할 수 있음
  • 객체의 수명 주기가 단순해집니다.
  • 바인더의 수정 불가 특성을 활용할 수 있습니다.

권장되지 않음: ID 기반 객체를 갖는 하나의 대규모 인터페이스

interface IManager {
   int getFooId();
   void beginFoo(int id); // clients in other processes can guess an ID
   void opFoo(int id);
   void recycleFoo(int id); // ownership not handled by type
}

권장됨: 개별 인터페이스

interface IManager {
    IFoo getFoo();
}

interface IFoo {
    void begin(); // clients in other processes can't guess a binder
    void op();
}

3. 일방향 메서드와 양방향 메서드를 혼합하여 사용하지 않기

[-Wmixed-oneway] 일방향 메서드와 일방향이 아닌 메서드를 혼합하여 사용하면 클라이언트와 서버가 스레딩 모델을 파악하는 데 어려움이 발생합니다. 구체적으로는, 특정 인터페이스의 클라이언트 코드를 읽을 때 각 메서드가 차단될지 여부를 일일이 살펴보아야 합니다.

4. 상태 코드 반환 지양

모든 AIDL 메서드에는 암시적 상태 반환 코드가 있으므로 메서드에서 상태 코드를 반환 값으로 사용하지 않아야 합니다. ServiceSpecificException 또는 EX_SERVICE_SPECIFIC을 참조하세요. 이러한 값은 관행적으로 AIDL 인터페이스에 상수로 정의되어 있습니다. 오류와 함께 맞춤 지연 시간이나 고유한 오류 데이터가 필요한 경우에만 맞춤 응답 객체가 오류를 나타내야 합니다. 자세한 내용은 오류 처리를 참고하세요.

5. 출력 매개변수로 배열을 사용하는 방식 지양

[-Wout-array] void foo(out String[] ret)와 같이 출력 매개변수로 배열을 사용하는 메서드는 클라이언트가 Java로 출력 배열 크기를 선언 및 할당해야 하기 때문에 서버에서 배열 출력의 크기를 선택할 수 없으므로 좋지 않은 방식입니다. 이 바람직하지 않은 동작은 Java에서 배열이 작동하는 방식(재할당되지 않음)으로 인해 발생합니다. 대신 String[] foo()와 같은 API를 사용하세요.

6. inout 매개변수 지양

[-Winout-parameter] inout 매개변수를 사용하면 in 매개변수도 out 매개변수처럼 보이기 때문에 클라이언트의 혼동을 유발할 수 있습니다.

7. 배열이 아닌 out 및 inout @nullable 매개변수 지양

[-Wout-nullable] Java 백엔드는 다른 백엔드와 달리 @nullable 주석을 처리하지 않기 때문에 out/inout @nullable T를 사용하면 여러 백엔드에서 일관되지 않은 동작이 발생할 수 있습니다. 예를 들어, Java가 아닌 백엔드는 out @nullable 매개변수를 null로 설정할 수 있지만 (C++에서는 std::nullopt로 설정하는 등) Java 클라이언트를 이를 null로 읽을 수 없습니다.

8. 고유한 요청 및 응답 사용

필요한 모든 매개변수를 하나의 입력 parcelable로 그룹화합니다. 기본 요소를 전달하는 대신 모든 인터페이스 메서드에 전용 요청 및 응답 parcelable을 만듭니다 (예: 별도의 변수를 전달하는 대신 ComputeResponse compute(in ComputeRequest request) 사용). 이렇게 하면 함수 서명을 변경하지 않고 나중에 새 인수를 추가할 수 있습니다. 이 패턴은 향후 매개변수가 더 추가될 수 있거나 메서드에 이미 4개 이상의 매개변수가 있는 경우에 적극 권장됩니다.

추가 입력이나 출력이 필요하지 않은 메서드는 이 제안의 이점을 누릴 수 없습니다. 각 사례를 명시적으로 고려하고 향후 변경사항에 유연하게 대처하면 지원 중단된 메서드가 줄어들고 이전 버전과 호환되는 코드의 복잡성이 줄어들 수 있습니다.

이 패턴을 사용하여 메서드를 만들지 않은 경우 요청 및 응답 parcelable을 사용하여 새 메서드를 만들고 이전 메서드를 지원 중단하여 이 패턴으로 전환할 수 있습니다. 예를 들면 다음과 같습니다.

void foo(int a, int b, int c); // original version, but deprecated in favor of the next version
void fooV2(in MyArg arg); // new version having int a, b, c, and d.

구조화된 parcelable

1. 사용하기 적합한 경우

전송해야 하는 데이터 형식이 여러 개인 경우에는 구조화된 parcelable을 사용합니다.

또는 지금 당장은 데이터 형식이 하나이지만 앞으로 더 늘어날 것으로 예상되는 경우에도 구조화된 parcelable을 사용하세요. 예를 들어, String username을 사용하는 대신 다음과 같이 확장 가능한 parcelable을 사용하세요.

parcelable User {
    String username;
}

이렇게 하면 추후에 다음과 같이 확장할 수 있습니다.

parcelable User {
    String username;
    int id;
}

2 명시적으로 기본값 제공

[-Wexplicit-default, -Wenum-explicit-default] 필드에는 명시적인 기본값을 제공합니다. 새 필드가 parcelable에 추가되면 이전 클라이언트 및 서버에서 이를 삭제하지만 새 클라이언트 및 서버의 경우 기본값이 자동으로 채워집니다.

3. 공급업체 확장에 ParcelableHolder 사용

기기 구현자가 확장해야 하는 AOSP parcelable를 정의하는 경우 객체에 ParcelableHolder 인스턴스를 삽입합니다. 이렇게 하면 병합 충돌을 일으키지 않고 확장 지점 역할을 할 수 있습니다. 이는 연결된 인터페이스 확장 프로그램과 유사하지만 구현자는 자체 인터페이스와 유형을 만들지 않고도 기존 parcelable과 함께 독점 parcelable을 포함할 수 있습니다.

4. 데이터 구조

  • AIDL은 모든 네이티브 백엔드(예: FeatureToScoreEntry[])에서 안전하게 변환되는 Map 유형을 기본적으로 지원하지 않으므로 parcelable의 배열이나 List을 사용하여 지도를 나타냅니다.
  • 원시 배열 대신 반복 필드에 parcelable 객체 배열을 사용하여 향후 병렬 배열이 필요하지 않도록 합니다.
  • 직렬화된 문자열이나 IPC를 통한 JSON 대신 강력한 유형의 parcelable 객체를 사용하세요.
  • 향후 확장을 위해 상태에 불리언 대신 enum을 사용합니다. 비트 마스크의 경우 일부 백엔드에서 번거로운 캐스팅을 방지하기 위해 enum 유형 대신 const int를 사용하세요.

구조화되지 않은 parcelable

1. 사용하기 적합한 경우

구조화되지 않은 parcelable은 Java에서는 @JavaOnlyStableParcelable을 통해, NDK 백엔드에서는 @NdkOnlyStableParcelable을 통해 사용할 수 있습니다. 일반적으로 이들은 오래된 기존 parcelable로, 구조화할 수 없습니다.

상수 및 enum

1. 비트필드에는 상수 필드 사용

비트필드에는 상수 필드를 사용해야 합니다 (예: 인터페이스의 const int FOO = 3;).

2. enum은 폐쇄형 세트여야 함

enum은 폐쇄형 세트여야 합니다. 참고: enum 요소는 인터페이스 소유자만 추가할 수 있습니다. 벤더 또는 OEM이 이러한 필드를 확장해야 하는 경우 다른 메커니즘이 필요합니다. 가능한 경우 항상 벤더 기능을 업스트리밍하는 방식을 사용하세요. 단, 경우에 따라 맞춤 벤더 값이 허용될 수도 있습니다(이때 벤더는 이를 또는 AIDL 자체를 버전 관리하는 메커니즘을 보유해야 하며, 서로 상충하지 않아야 하고, 이러한 값을 서드 파티 앱에 노출해서는 안 됩니다).

3. 'NUM_ELEMENTS'와 같은 값 지양

enum에는 버전이 부여되므로 몇 개의 값이 있는지를 나타내는 값은 사용하지 않아야 합니다. C++에서는 enum_range<>를 사용하여 이 제한을 우회할 수 있습니다. Rust에서는 enum_values()를 사용하세요. Java에는 아직 해결 방법이 없습니다.

권장되지 않음: 숫자가 지정된 값 사용

@Backing(type="int")
enum FruitType {
    APPLE = 0,
    BANANA = 1,
    MANGO = 2,
    NUM_TYPES, // BAD
}

4. 불필요한 접두사와 접미사 사용 지양

[-Wredundant-name] 상수와 열거자에 불필요하거나 반복적인 접두사와 접미사를 사용하지 않습니다.

권장되지 않음: 불필요한 접두사 사용

enum MyStatus {
    STATUS_GOOD,
    STATUS_BAD // BAD
}

권장됨: enum에 직접 이름 지정

enum MyStatus {
    GOOD,
    BAD
}

FileDescriptor

[-Wfile-descriptor] FileDescriptor를 AIDL 인터페이스의 인수 또는 반환 값으로 사용하는 방식은 지양해야 합니다. 특히 Java에서 AIDL을 구현하는 경우에 이렇게 하면 주의 깊게 처리하지 않는 한 파일 설명자가 누출될 수 있습니다. FileDescriptor를 받는 경우에는 더 이상 사용하지 않을 경우 수동으로 종료해야 합니다.

네이티브 백엔드의 경우에는 FileDescriptor가 자동으로 종료되는 unique_fd에 매핑되므로 괜찮습니다. 단, 백엔드 언어로 무엇을 사용하든, FileDescriptor를 사용하면 추후 백엔드 언어를 변경하기가 어려워지므로 사용하지 않는 것이 좋습니다.

대신 자동으로 종료되는 ParcelFileDescriptor를 사용하세요.

변수 단위

참고 문서를 보지 않고도 단위를 알아볼 수 있도록 이름에 변수 단위를 포함합니다.

long duration; // Bad
long durationNsec; // Good
long durationNanos; // Also good

double energy; // Bad
double energyMilliJoules; // Good

int frequency; // Bad
int frequencyHz; // Good

타임스탬프는 참조를 나타내야 함

타임스탬프(뿐 아니라 모든 단위)는 단위와 참조 지점을 명확히 나타내야 합니다.

/**
 * Time since device boot in milliseconds
 */
long timestampMs;

/**
 * UTC time received from the NTP server in units of milliseconds
 * since January 1, 1970
 */
long utcTimeMs;

동시 실행 및 비동기 작업

차단을 방지하기 위해 비동기 (oneway) 인터페이스로 장기 실행 작업을 처리합니다.

서비스가 클라이언트를 신뢰하지 않는 경우 클라이언트로부터 수신하는 모든 콜백은 oneway 인터페이스여야 합니다. 이렇게 하면 클라이언트가 서비스를 무기한 차단할 수 없습니다.

결과를 가져오기 위한 전달 호출, 입력 인수, 콜백 인터페이스로 구성된 비동기 API를 구조화합니다. 인수 추천은 고유한 요청 및 응답 사용을 참고하세요.