개발 취재록

안드로이드 앱에서 마이크를 계속 켜두려면? 부팅 자동 시작·SpeechRecognizer 오프라인까지 구현 전 체크리스트

PC에서 호출어를 듣고 답을 읽어주던 개인 음성비서(voice-assistant)를, 충전 거치해 둔 안드로이드 태블릿에서 항상 듣는 앱으로 옮기려 했다. 구현 전에 "상시 마이크에 필요한 것" 목록부터 받았다. 그런데 공식 문서와 맞춰 보니 부팅 자동 시작내장 음성 인식 = 로컬이라는 두 항목이 그대로는 틀렸다.

이 글은 설계 단계의 체크리스트다. 실기기에서 돌려본 결과는 없고, 사실 판단은 모두 안드로이드 공식 문서(2026-09-23 확인)에 기댄다. 앱 구조(귀·입과 두뇌 분리, MockBrain) 이야기는 Claude Code를 안드로이드 태블릿에 붙이려는데, 음성비서 앱은 어디부터 만들까?에 따로 정리했다.

결론

상시 음성 감지 앱을 만들기 전에 확인할 것:

#항목판정
1android:foregroundServiceType="microphone" 서비스 + RECORD_AUDIO·FOREGROUND_SERVICE·FOREGROUND_SERVICE_MICROPHONE 선언필수 (targetSdk 34 이상은 타입·타입별 권한 선언 의무)
2서비스는 화면에 보이는 Activity에서 마이크 권한을 받은 뒤 시작필수. RECORD_AUDIO는 "사용 중에만" 권한이라 백그라운드에서 새로 시작하면 막힐 수 있다
3RECEIVE_BOOT_COMPLETED로 부팅 직후 녹음 서비스 자동 시작안 된다. BOOT_COMPLETED 수신기는 microphone 타입 FGS를 띄울 수 없다(Android 14부터). 부팅 후 앱을 한 번 여는 흐름을 전제로 설계
4POST_NOTIFICATIONSFGS 시작의 필수 조건은 아님. 거부하면 알림이 보이는 위치만 달라진다. 알림 자체는 FGS에 필요
5배터리 최적화 예외·충전 거치·WAKE_LOCK도움은 되지만 무중단 보증은 아님. 화면 잠금 상태 장시간 동작은 직접 측정
6STT를 SpeechRecognizer.createSpeechRecognizer()로컬 보장 아님. 원격 서버로 오디오를 보낼 수 있다. 오프라인이 필요하면 API 31+의 온디바이스 인식기를 명시적으로 쓴다
7호출어 감지(AudioRecord 상시)와 STT가 마이크를 동시에 쓰는가보장 없음. 첫 구현은 "호출어 감지 → AudioRecord 해제 → STT → TTS → 호출어 재개" 순차 전환으로

온디바이스 인식 쪽 뼈대는 이 정도다(API 31 이상).

val recognizer = if (SpeechRecognizer.isOnDeviceRecognitionAvailable(context)) {
    SpeechRecognizer.createOnDeviceSpeechRecognizer(context)
} else {
    // 로컬 필수라면 여기서 조용히 일반 인식기로 넘어가지 말고 오류를 보여준다
    null
}

isOnDeviceRecognitionAvailable()true여도 한국어 모델이 준비됐다는 뜻까지는 아니다. API 33의 checkRecognitionSupport()와 모델 다운로드 API로 언어별 상태를 따로 확인하고, 네트워크를 실제로 끈 상태에서 인식해 보는 것이 최종 판정이다. RecognizerIntent.EXTRA_PREFER_OFFLINE=true는 구현에 따라 무시될 수 있어 오프라인 보증으로 쓰면 안 된다. (SpeechRecognizer, RecognizerIntent)

상황

만들려던 앱의 구조는 이렇다. Foreground Service(사용자에게 알림을 띄운 채 계속 도는 서비스)가 마이크를 붙잡고, openWakeWord 호출어 모델(PC 버전과 같은 hey_jarvis)이 "헤이 자비스"를 감지하면 안드로이드 SpeechRecognizer로 말을 텍스트로 바꾸고, 답을 TextToSpeech로 읽는다. 답을 만드는 부분은 일단 MockBrain이라는 가짜 구현으로 둔다.

기기는 충전기에 꽂아 둔 전용 태블릿으로 쓰기로 했고, 그래서 배터리 문제는 대부분 사라진다고 봤다.

증상

구현 전에 받은 준비 목록은 다음과 같았다.

목록만 보면 선언 몇 줄로 끝날 것 같다. 이 목록 그대로 앱을 짜면 부팅 후 녹음이 시작되지 않거나, 오프라인이라고 믿은 인식이 실은 서버로 오디오를 보내는 상황을 만날 수 있다.

원인

목록의 항목들이 "선언하면 된다"와 "언제 시작하느냐에 따라 막힌다"를 구분하지 않았다.

1. 부팅 자동 시작. 적법하게 시작된 microphone FGS가 백그라운드로 내려간 뒤에도 녹음을 이어가는 것과, 백그라운드에서 새로 시작하는 것은 다르다. RECORD_AUDIO는 while-in-use(앱을 쓰는 동안만) 제한을 받는 권한이고, 백그라운드 FGS 시작 예외와 마이크 권한 예외는 별개다. Android 14 이상을 대상으로 하는 앱이 이 조건을 어기고 microphone FGS를 올리면 SecurityException이 날 수 있다. 부팅 브로드캐스트를 받은 시점의 앱은 화면에 보이는 Activity가 없다. 실제로 Android 15 동작 변경 문서는 BOOT_COMPLETED 수신기가 띄울 수 없는 FGS 타입 목록에 microphone을 넣고, 이 제한이 Android 14부터 있었다고 적어 두었다. (FGS 타입, 백그라운드 시작 제한, Android 15 동작 변경)

2. 내장 STT = 로컬. 기본 createSpeechRecognizer()는 로컬 전용이 아니다. 공식 레퍼런스는 이 인식기가 원격 서버로 오디오를 보낼 수 있고, 지속 인식 용도로 설계되지 않았다고 적고 있다. 온디바이스 인식은 API 31부터 isOnDeviceRecognitionAvailable()로 확인하고 createOnDeviceSpeechRecognizer()로 따로 만들어야 한다. (SpeechRecognizer)

3. 작은 교정 두 가지. POST_NOTIFICATIONS는 알림 표시 권한이지 FGS 시작 조건이 아니다. (알림 권한) 그리고 microphone 타입 자체는 Android 11의 카메라·마이크 FGS 요건에서 이미 등장한다. Android 14에서 바뀐 것은 targetSdk 34 이상 앱이 타입과 타입별 권한을 반드시 선언해야 한다는 점이다. (Android 11 변경)

4. 목록에 없던 것 — 마이크 동시 사용. 호출어 감지는 AudioRecord로 마이크를 계속 열어 두고, SpeechRecognizer는 자기 마이크 입력을 따로 연다. Android 10 이후 입력 공유 정책에서는 두 일반 앱이 동시에 실제 오디오를 받지 못하고 한쪽이 무음을 받을 수 있다. 같은 앱의 AudioRecord와 특정 인식 서비스 조합이 항상 실패한다고까지는 단정할 수 없지만, 동시에 된다는 보장도 없다. 오디오 포커스는 출력 중재라 이 문제를 풀어주지 않는다. (오디오 입력 공유)

해결

구현 전에 체크리스트를 이렇게 고쳐 쓴다.

시작 흐름

계속 듣기

STT

마이크 전환

툴체인

덤 — "거치 전용기라 배터리 걱정 없음"의 범위

충전기에 꽂아 두면 배터리가 닳는 문제는 사라진다. 하지만 이 글의 1~4번 문제(시작 시점 제한, 인식기의 서버 전송, 마이크 공유)는 전원과 무관하다. "거치 전용"은 배터리 항목 하나만 지워 주는 조건으로 읽는 게 안전하다.

취재 후기 — 선언 몇 줄이면 끝날 줄 알았던 목록

*실기기 재현은 아직 없다. 네 사람은 받은 준비 목록을 공식 문서와 한 줄씩 맞춰 봤다.*
박도은
PC에서 "헤이 자비스" 하면 듣고 답을 읽어주는 음성비서 있잖아. 그걸 충전기에 꽂아 둔 태블릿에서 항상 듣게 만들려고 준비 목록부터 받아 왔어.
정바다
목록 보니까 간단하던데. 마이크 타입 Foreground Service, 그러니까 알림을 띄운 채 계속 도는 서비스 하나에 권한 네 개, 부팅 자동 시작은 RECEIVE_BOOT_COMPLETED. 선언만 하면 전원 켜자마자 듣겠지.
정하늘
선언과 시작 시점은 따로 봐야 해. 백그라운드에서 마이크 서비스를 새로 띄우는 게 허용되는지, 공식 문서의 백그라운드 시작 제한 항목부터 확인하자.
박도은
찾아봤어. RECORD_AUDIO는 앱을 쓰는 동안만 유효한 권한이라, 화면에 앱이 없는 상태에서 마이크 서비스를 새로 만들면 Android 14 조건에서는 SecurityException이 날 수 있대. 게다가 부팅 신호를 받는 수신기는 마이크 타입 서비스를 아예 못 띄운다고 목록에 박혀 있어.
정하늘
부팅 직후엔 화면에 우리 앱이 없잖아. 그러니 부팅 수신만으로 녹음을 시작한다는 가설은 틀렸어. 앱을 한 번 열어서 시작하는 흐름이 기본이야.
박도현
계속 도는 것과 새로 시작하는 걸 한 줄로 묶은 게 실수였어. 안드로이드는 이미 켜진 마이크는 봐주지만, 뒤에서 몰래 새로 켜는 건 막는다고 생각하면 돼.
정바다
그럼 STT는 문제없지. 내장 SpeechRecognizer, 음성을 글자로 바꿔 주는 안드로이드 기본 부품이잖아. 무료고 기기 안에서 도니까 오프라인도 될 거야.
정하늘
"기기 안에서 돈다"가 근거 없는 전제야. 레퍼런스에 기본 인식기가 오디오를 어디로 보내는지 적혀 있을 테니 그걸로 가리자.
박도은
기본 createSpeechRecognizer()는 원격 서버로 오디오를 보낼 수 있다고 적혀 있어. 기기 안에서만 돌리려면 API 31부터 있는 createOnDeviceSpeechRecognizer()를 따로 써야 하고.
정하늘
그럼 판정은 "내장 = 로컬"이 아니라 "온디바이스를 명시해야 로컬"이야. 그것도 한국어 모델이 준비됐는지, 비행기 모드에서 실제로 되는지 봐야 끝나.
박도현
여기서 제일 위험한 건 온디바이스가 안 될 때 일반 인식기로 조용히 넘어가는 코드야. 그러면 서버로 보내 놓고도 오프라인으로 됐다고 믿게 되거든.
정바다
알림 권한 POST_NOTIFICATIONS도 목록에 있었으니 이것까지 받아야 서비스가 뜨는 거지? 안드로이드 13부터 알림 띄우려면 물어봐야 하잖아.
박도은
문서엔 그 권한이 서비스 시작 조건은 아니라고 나와. 거부하면 알림이 어디에 보이는지만 달라진대.
정하늘
그럼 필수 목록에서 빼고 권장으로 내리자. 알림 자체는 서비스에 필요하니까 만들긴 해야 하고.
정바다
목록에 없던 것도 하나 걸려. 호출어는 AudioRecord로 마이크를 계속 열고, 인식기도 자기 마이크를 연다며. 둘이 동시에 마이크를 잡으면 한쪽이 무음을 받는 거 아냐?
박도은
오디오 입력 공유 문서를 보니 일반 앱 둘이 동시에 실제 소리를 받지 못하고 한쪽이 무음을 받을 수 있대. 같은 앱 안의 조합이 항상 실패한다고까진 안 쓰여 있고.
정하늘
된다는 보장도 없다는 뜻이네. 첫 구현은 호출어가 잡히면 AudioRecord를 놓고, 인식이 끝나거나 실패하면 다시 호출어로 돌아오는 순차 전환으로 가자.
박도현
배터리는 충전 거치로 지워지지만, 시작 시점이나 마이크 공유는 전원과 상관없는 문제야. 조건 하나가 목록 전체를 해결해 주진 않아.
박도은
확인하려고 개발 PC를 봤는데 JDK가 16이야. 최신 안드로이드 빌드는 17 이상이 필요하고, 기기에 설치하는 도구 adb도 SDK도 Android Studio도 없어.
정하늘
그래서 오늘 판정은 문서로 고친 체크리스트까지야. 실제로 잠금 화면에서 몇 시간 듣는지, 첫 음절이 잘리는지는 앱을 올려서 로그로 재야 해.
박도현
상시 음성 앱의 준비물은 "무엇을 선언하나"보다 "언제 시작하고, 소리가 어디로 가고, 마이크를 누가 쥐나"를 먼저 묻는 목록이어야 해.