본문 바로가기
안드로이드

AI 에이전트가 내 앱 기능을 직접 호출하게 하는 법

by 안드뽀개기 2026. 5. 26.
반응형

어떤 프로젝트에 맞는 내용인가

이 글은 Room 기반 로컬 데이터를 다루는 앱을 운영 중이고, 앱 기능을 기기 내 AI 에이전트에 노출하고 싶은 Android 개발자를 대상으로 합니다. 최소 SDK 26 이상이 권장되며, Kotlin과 KSP(Kotlin Symbol Processing)를 사용한다고 가정합니다. 2026년 5월 기준 AppFunctions는 alpha 단계이므로 프로덕션 앱에 바로 투입하기보다 신규 기능 실험 용도로 접근하는 것이 현실적입니다.


이런 상황을 만난 적 있으신가요

할 일 관리 앱을 운영하다 보면 이런 요청이 꼭 들어옵니다. "기기 어시스턴트한테 '내일 오후 3시에 회의 준비 추가해줘'라고 말했는데, 왜 우리 앱에는 반영이 안 되나요?"

기기 AI 에이전트는 앱의 UI를 직접 조작하거나 앱 내부 데이터에 접근할 방법이 없습니다. Intent나 Deep Link를 통해 앱을 실행시키는 수준이 전부였습니다. AppFunctions는 이 한계를 정면으로 해결하려는 시도입니다. 앱의 특정 메서드를 에이전트가 호출 가능한 도구로 등록하면, 앱이 실행 중이 아니어도 시스템이 해당 기능을 대신 깨워서 실행시킵니다.


의존성 추가

먼저 build.gradle.kts에 다음을 추가합니다.

// build.gradle.kts (app 모듈)
plugins {
    id("com.google.devtools.ksp")
}

dependencies {
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha01")
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha01")
}

KSP를 이미 쓰고 있다면 플러그인 중복 없이 dependency만 추가하면 됩니다. KSP를 쓰지 않는 프로젝트라면 루트 build.gradle.kts에도 KSP 플러그인 버전을 선언해야 합니다.


AppFunction 선언하기

// TaskAppFunctions.kt
import androidx.appfunctions.AppFunction
import androidx.appfunctions.AppFunctionContext

class TaskAppFunctions(
    private val repository: TaskRepository
) {

    @AppFunction(
        description = "현재 완료되지 않은 작업 목록을 반환합니다."
    )
    suspend fun getActiveTasks(
        context: AppFunctionContext
    ): List<TaskSummary> {
        return repository.getActiveTasks()
            .map { TaskSummary(id = it.id, title = it.title, dueDate = it.dueDate) }
    }

    @AppFunction(
        description = "새 작업을 추가합니다. dueDate는 ISO-8601 형식(예: 2026-05-27)으로 입력합니다."
    )
    suspend fun addTask(
        context: AppFunctionContext,
        title: String,
        dueDate: String? = null
    ): TaskSummary {
        val inserted = repository.insert(Task(title = title, dueDate = dueDate))
        return TaskSummary(id = inserted.id, title = inserted.title, dueDate = inserted.dueDate)
    }
}

data class TaskSummary(
    val id: Long,
    val title: String,
    val dueDate: String?
)

description 파라미터가 핵심입니다. AI 에이전트는 이 설명을 읽고 언제 이 함수를 호출할지 결정합니다. MCP의 tool description과 같은 역할이므로 모호한 표현 대신 행동 조건과 파라미터 형식을 명시적으로 쓰는 게 좋습니다.


AppFunctionService 연결하기

// TaskAppFunctionService.kt
import androidx.appfunctions.AppFunctionService

class TaskAppFunctionService : AppFunctionService() {

    // 실제 앱에서는 Hilt 등 DI 컨테이너를 통해 주입하세요
    private val taskFunctions by lazy {
        TaskAppFunctions(
            repository = (application as MyApp).taskRepository
        )
    }

    override fun getAppFunctionHandlers(): List<Any> = listOf(taskFunctions)
}

AppFunctionService는 내부적으로 Service를 상속합니다. 앱이 포그라운드에 없어도 시스템이 이 서비스를 통해 함수를 실행할 수 있습니다.

AndroidManifest.xml에는 다음과 같이 선언합니다.

<service
    android:name=".TaskAppFunctionService"
    android:exported="true"
    android:permission="android.permission.EXECUTE_APP_FUNCTIONS">
    <intent-filter>
        <action android:name="androidx.appfunctions.action.EXECUTE_APP_FUNCTION" />
    </intent-filter>
</service>

EXECUTE_APP_FUNCTIONS 권한을 요구하도록 선언하는 부분이 중요합니다. 이 권한은 시스템 에이전트만 갖고 있기 때문에 일반 앱이 임의로 함수를 호출하는 것을 막아줍니다.


adb로 직접 테스트하기

에이전트 통합은 아직 비공개 프리뷰이므로, 개발 중에는 adb를 이용해 함수 실행을 직접 테스트할 수 있습니다.

# 등록된 AppFunction 목록 확인
adb shell cmd appfunctions list --package com.example.myapp

# addTask 함수 직접 호출
adb shell cmd appfunctions execute \
  --package com.example.myapp \
  --function addTask \
  --params '{"title":"장보기","dueDate":"2026-05-28"}'

리스트 조회 결과에 정의한 함수명과 description이 보인다면 등록이 정상적으로 된 것입니다. 반환값은 JSON 형태로 셸에 출력됩니다.


Before / After로 보는 차이

Before: 사용자가 어시스턴트에게 "내일 할 일 추가해줘"라고 말하면, 에이전트는 앱을 단순히 실행시키는 것 외에는 아무것도 할 수 없었습니다. 결국 사용자가 앱을 열어 직접 입력해야 했습니다.

After: AppFunctions를 등록하면 에이전트가 addTask 함수를 직접 호출하고, Room 데이터베이스에 실제 레코드가 삽입됩니다. 사용자가 앱을 열면 방금 추가된 항목이 목록에 이미 보입니다.

코드 관점에서도 변화가 있습니다. 이전에는 Intent 처리, Deep Link 파싱, 파라미터 검증 코드가 Activity나 ViewModel에 흩어져 있었습니다. 이제는 @AppFunction으로 선언한 함수 하나가 그 역할을 담당하고, 파라미터 파싱은 컴파일 타임에 생성된 코드가 처리합니다.


주의사항과 한계

가장 큰 제약은 아직 알파라는 점입니다. API 시그니처가 GA 전에 바뀔 가능성이 있으므로 production 앱에 바로 적용하는 건 권장하지 않습니다.

파라미터 타입에도 제약이 있습니다. 현재 알파에서는 기본 타입(String, Int, Boolean)과 해당 타입의 List, 단순한 data class 조합만 지원합니다. Flow나 sealed class 계층은 지원되지 않습니다.

보안 측면도 짚고 넘어가야 합니다. getActiveTasks 같은 함수는 앱 내 전체 데이터를 노출할 수 있습니다. 호출자 신원을 확인하는 API가 향후 제공될 예정이지만, 현 알파에서는 EXECUTE_APP_FUNCTIONS 권한 체크에만 의존하는 상황입니다.


지금 바로 시작하는 첫 번째 단계

전체 구조를 바꾸지 않아도 됩니다. 기존 앱에서 Repository 레이어 하나를 골라 읽기 전용 조회 메서드 하나만 @AppFunction으로 선언해보세요.

TaskAppFunctions.kt 파일을 새로 만들고, 위의 getActiveTasks 예시를 그대로 붙여넣은 뒤 Repository만 자신의 것으로 교체하세요. 이후 adb shell cmd appfunctions list로 함수가 등록됐는지 확인하는 것까지가 오늘의 목표입니다. 데이터를 변경하는 함수는 그 다음에 추가해도 충분히 늦지 않습니다.

※ 본 글은 정보 제공 목적이며 특정 제품·서비스의 추천이 아닙니다.

반응형