GDG행사인 Google I/O Extended 에서 핸즈온 세션에 운빨로 참여할 수 있는 기회가 생겼다. 내용은 “Android App Functions: 내 앱을 에이전트에게 연결하기” 였다.
2026 Google I/O 에서 발표한 내용으로, App Functions를 Android의 온디바이스 MCP 방식으로 나왔다고 한다. 이전부터 AppFunctions는 존재했지만, Gemini와 연결하는 실제 개발 경험을 이번에 본격적으로 공개했다.
해당 영상을 보면, 간단하게 첫 번째 앱인 채팅 앱을 구현하고, 사용자가 AI에게 부탁을 하는 두 번째 앱에, Gemini API Key를 넣어 실행하면, 두 번째 앱에서 간단하게 프롬프트로 “~~에게 ~~라고 보내줘” 라고 말하면 실제 앱에서 보내주는 모습이다.
GitHub - android/appfunctions: Android AppFunctions samples · GitHub
안드로이드에서 샘플로 개발한 앱인데, 해당 앱을 다운 받아 실행할 수 있다.
AppFunctions가 그래서 뭔데?
앱이 온디바이스 MCP 서버처럼 동작해 Gemini 같은 에이전트와 함께 사용할 도구 역할을 하는 기능을 제공한다. AppFunctions를 정의하면, 앱에서 제공하는 기능들을 에이전트가 실행할 권한을 가져 사용자 ↔ 에이전트 상호작용을 통해 앱에서 자동으로 실행할 수 있다.
AppFunctions는 Android 16 이상을 실행하는 기기에서 사용할 수 있다. (웬만하면 16보단 17에서 실행하는 걸 추천한다) 나는 Android Studio Panda 버전은 안되고, Quail 2버전을 했을 때 되었다.
AppFunctions를 최적화하고 테스트, 디버깅을 위한 ADB 명령어 스킬을 제공한다. 아래 링크에서 그 스킬을 받을 수 있다. https://github.com/android/skills/tree/main/device-ai/appfunctions
아래 세 방법을 적용해 AppFunctions를 쓸 수 있다
- 기존의 함수에서 @AppFunction 어노테이션만 붙여준다면 그대로 그 함수의 기능을 사용할 수 있다.
- isDescribedByKDoc = true 를 통해, KDoc 주석을 LLM에게 줄 수 있다
- 앱 프로세스 안에서 내 권한을 통해 실행된다. 화면을 띄우지 않고 백그라운드 실행도 가능하다.
android 공식 문서에서 다양한 예시를 들어주는데, 주로 아래와 같은 앱에서 쓸 수 있다
- 할 일 관리 및 생산성 앱
- 오늘 오후 5시에 회사에서 택배를 찾아오라고 알려줘
- 미디어 및 엔터테인먼트
- 올해의 인기 락 메탈 앨범으로 새 재생목록을 만들어줘
- 앱 간 워크플로우
- 이메일에서 국수 레시피를 찾아 재료를 내 쇼핑 목록에 추가해줘
- 캘린더 및 일정
- 다음 주 월요일 오후 6시에 엄마 생일파티를 내 캘린더에 추가해줘
/**
* 제목, 기한, 위치를 포함한 새로운 할일이나 알림을 만든다.
* Create a new task or reminder with a title, due time, and location.
*
* @param title The descriptive title of the task (e.g., "Pick up my package").
* @param dueDateTime The specific date and time when the task should be completed.
* @param location The physical location associated with the task (e.g., "Work").
* @return The created Task
*/
@AppFunction(isDescribedByKDoc = true)
suspend fun createTask(
title: String,
dueDateTime: LocalDateTime? = null,
location: String? = null,
): Task = TODO()
- 주석의 내부는 KDoc으로, 함수에 대한 설명과 각각의 파라미터에 대한 설명, 그리고 Return 값에 대한 설명을 나타낸다
- 대략적으로 투두를 만드는 함수이다.
- 호출자인 LLM앱이 관련 작업 관리 앱을 식별하고 함수를 호출해 작업을 만든다. 제목, 시간, 위치 필드는 사용자의 프롬프트에 따라 자동으로 채워진다
AppFunctions 작동 방식
앱이 AppFunctions를 에이전트와 공유하고 실행하는 흐름을 보여준다.

- 앱에서 AppFunctions를 사용해 ‘메모 작성’ 또는 ‘메세지 보내기’와 같은 기능을 사용할 수 있도록 빌드한다.
- AppFunctions 라이브러리가 앱에 선언된 모든 AppFunctions를 나열하는 XML 파일을 생성한다. OS는 이 파일을 사용해 사용가능한 AppFunctions를 색인한다
- 에이전트가 쿼리를 통해 AppFunction 메타데이터를 검색할 수 있다. 함수별 KDoc 외에도 개발자는 앱 메타데이터에 운영 패턴과 제약 조건을 정의해 여러 도구에서 에이전트 오케스트레이션을 안내할 수 있다.
- 사용자 프롬프트를 기반으로 에이전트가 적절한 파라미터를 사용해 적절한 AppFunction을 선택하고 실행한다.
우리가 만든 앱(에이전트가 아닌 앱)에서는 어노테이션과 XML을 통해서 기능한다. 리플렉션과 런타임 등록 없이, 앱 설치만으로 시스템이 인지하게 되게 된다.
호출자(에이전트)가 AppFunctionManager를 사용해 이런 색인 함수를 검색하고 호출할 수 있다. 호출자는 isAppFunctionEnabled(packagename, functionId)를 사용해 타겟 앱 내에서 특정 함수가 사용 설정 되어있는지 확인할 수 있다. 호출자의 앱에서 AppFunctions를 사용하려면 EXECUTE_APP_FUNCTIONSpermission 권한이 필요하다. 이 권한은 함수 목록 조회, 실행 등을 포함한 권한이다.
다만 현재는 실험용 기능이여서 제한된 사용이 가능하다. 이를 위해선 특수한 권한을 가진 채로 실행해야한다.
cd agent
./run_privileged.sh --build --flavor retail --api-key (API Key 넣기)
만약 공식 샘플 앱을 받아온다면, 위와 같은 방식을 통해서 실행할 수 있다. (Mac 기준) 위에 링크가 있다. run_privileged.sh를 통해 권한을 부여해 실행할 수 있다.
실제 프로젝트에 적용
// build.gradle.kts
plugins {
id("com.google.devtools.ksp")
}
android {
compileSdk = 36 // 36 이상 필수
}
dependencies {
implementation("androidx.appfunctions:appfunctions:1.0.0-alpha10")
// Kotlin 소스를 쓴다면 KSP로 컴파일러를 추가
ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha10")
}
위와 같은 의존성을 추가해야 한다.
/**
* 누렁이의 서현역 실내 길 안내를 시작하는 App Function 서비스입니다.
*/
@RequiresApi(36)
@AppFunctionServiceEntryPoint(
serviceName = "NureongiAppFunctionService",
appFunctionXmlFileName = "nureongi_navigation_app_functions",
)
abstract class BaseNureongiAppFunctionService : AppFunctionService() {
/**
* 서현역 안의 출발지에서 목적지까지 접근성 길 안내를 시작하고 현재 단계를 알림에 표시합니다.
* 사용자가 "3번 출구에서 여자 화장실까지 안내해 줘"처럼 출발지와 목적지를 말하면 호출합니다.
*
* @param startLocation 사용자가 출발하는 서현역 내 장소 이름입니다.
* @param destination 사용자가 도착하려는 서현역 내 장소 이름입니다.
* @return 실제로 확인된 출발지, 목적지와 첫 안내 정보입니다.
*/
@AppFunction(isDescribedByKDoc = true)
fun startGuidance(
startLocation: String,
destination: String,
): NavigationStartResult {
if (!NavigationNotificationController.canPostNotifications(this)) {
throw AppFunctionPermissionRequiredException(
"누렁이 앱을 열고 알림 권한을 허용한 뒤 다시 안내를 요청해 주세요.",
)
}
val route = try {
NavigationRouteResolver().resolve(startLocation, destination)
} catch (exception: IllegalArgumentException) {
throw AppFunctionInvalidArgumentException(exception.message)
}
// 시작점과 목적지를 설정했을 때, 문제가 없다면 네비게이션 경로를 생성한다
NavigationNotificationController.start(this, route)
// 네비게이션을 시작하고, 상단바에 알림을 생성한다
val firstStep = route.steps.first()
// 첫 시작을 설정하고, 결과를 반환한다
return NavigationStartResult(
startLocation = route.startName,
destination = route.destinationName,
currentInstruction = firstStep.guideMessage,
remainingDistance = firstStep.remainingDistanceText,
totalSteps = route.steps.size,
)
}
}
// 반환 값인 NavigationStartResult는 아래에 있다.
- AppFunctionServiceEntryPoint 는 AppFunction를 담을 서비스들을 선언한다.
- KSP가 이 클래스를 상속한 구현서비스(NureongiAppFunctionService)를 생성하게 된다.
- 주석(KDoc)을 통해서 에이전트에게 해당 내용을 전달한다.
- appFunctionXmlFileName 은 생성될 XML 파일명이다.
- AppFunction은 실제로 에이전트가 실행할 수 있는 함수이다.
- isDescribedByKDoc 을 통해 에이전트에게 해당 내용을 전달할 수 있다.
- 예외도 복구 지시로 가능하다. @throws 에 “if thrown, ask the user to …” 를 적어서 유저에게 질문할 수 있도록 한다. (예를 들면, 목적지를 빼먹었다거나, 목적지와 출발지가 같아 예외가 발생하면 다시 설정하라고 할 수 있다.)
추가적으로 DI(Hilt, Dagger 등)를 그대로 활용할 수 있다.
/**
* 시작된 길 안내의 현재 상태입니다.
*/
@AppFunctionSerializable(isDescribedByKDoc = true)
data class NavigationStartResult(
/** 확인된 출발지 이름입니다. */
val startLocation: String,
/** 확인된 목적지 이름입니다. */
val destination: String,
/** 지금 따라야 할 첫 번째 안내입니다. */
val currentInstruction: String,
/** 목적지까지 남은 거리입니다. */
val remainingDistance: String,
/** 목적지까지의 전체 안내 단계 수입니다. */
val totalSteps: Int,
)
- 프로퍼티의 KDoc도 읽는다. 각 필드 위의 주석이 그대로 필드 설명으로 XML에 들어가게 된다.
- 지원하는 타입은 기본형(Primitive, String, Int 등), List<T>에다가 Uri, PendingIndent와 같은 Android 타입도 지원해준다.
<service
android:name=".appfunctions.NureongiAppFunctionService"
android:exported="true"
android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
tools:targetApi="36">
<property
android:name="android.app.appfunctions.schema"
android:value="app_functions_schema.xsd" />
<property
android:name="android.app.appfunctions.v2"
android:value="nureongi_navigation_app_functions.xml" />
<intent-filter>
<action android:name="android.app.appfunctions.AppFunctionService" />
</intent-filter>
</service>
그리고 KSP가 생성한 서비스를 Manifest에 등록해야한다.
내가 사용하던 앱을 사용해 세부적인 부분은 이해가 안되도, abstract class 를 선언한 선언부와 어노테이션, suspend 함수만 이해해도 어느 정도는 쓸 수 있다고 생각한다.
실제 동작하는 화면은 아래와 같다. (한국어 설정을 따로 안해서 영어로 보이는점...)
https://youtube.com/shorts/zzk-mJrD_wo?si=9YSEWskdQ1H_AJu-
GDG에서 핸즈온 발표 해주신 기민님 감사합니다..!

'우테코 8기 > Android' 카테고리의 다른 글
| Instrumented Test (계측 테스트) (0) | 2026.08.22 |
|---|---|
| Dagger 기본 개념과 @Inject, @Component (0) | 2026.08.01 |
| Compose에서 접근성 제공하기(1) - semantics, contentDescription (0) | 2026.07.12 |
| Android Context (2) | 2026.06.14 |