안드로이드 앱에서 마이크를 계속 켜두려면? 부팅 자동 시작·SpeechRecognizer 오프라인까지 구현 전 체크리스트
PC에서 호출어를 듣고 답을 읽어주던 개인 음성비서(voice-assistant)를, 충전 거치해 둔 안드로이드 태블릿에서 항상 듣는 앱으로 옮기려 했다. 구현 전에 "상시 마이크에 필요한 것" 목록부터 받았다. 그런데 공식 문서와 맞춰 보니 부팅 자동 시작과 내장 음성 인식 = 로컬이라는 두 항목이 그대로는 틀렸다.
이 글은 설계 단계의 체크리스트다. 실기기에서 돌려본 결과는 없고, 사실 판단은 모두 안드로이드 공식 문서(2026-09-23 확인)에 기댄다. 앱 구조(귀·입과 두뇌 분리, MockBrain) 이야기는 Claude Code를 안드로이드 태블릿에 붙이려는데, 음성비서 앱은 어디부터 만들까?에 따로 정리했다.
결론
상시 음성 감지 앱을 만들기 전에 확인할 것:
| # | 항목 | 판정 |
|---|---|---|
| 1 | android:foregroundServiceType="microphone" 서비스 + RECORD_AUDIO·FOREGROUND_SERVICE·FOREGROUND_SERVICE_MICROPHONE 선언 | 필수 (targetSdk 34 이상은 타입·타입별 권한 선언 의무) |
| 2 | 서비스는 화면에 보이는 Activity에서 마이크 권한을 받은 뒤 시작 | 필수. RECORD_AUDIO는 "사용 중에만" 권한이라 백그라운드에서 새로 시작하면 막힐 수 있다 |
| 3 | RECEIVE_BOOT_COMPLETED로 부팅 직후 녹음 서비스 자동 시작 | 안 된다. BOOT_COMPLETED 수신기는 microphone 타입 FGS를 띄울 수 없다(Android 14부터). 부팅 후 앱을 한 번 여는 흐름을 전제로 설계 |
| 4 | POST_NOTIFICATIONS | FGS 시작의 필수 조건은 아님. 거부하면 알림이 보이는 위치만 달라진다. 알림 자체는 FGS에 필요 |
| 5 | 배터리 최적화 예외·충전 거치·WAKE_LOCK | 도움은 되지만 무중단 보증은 아님. 화면 잠금 상태 장시간 동작은 직접 측정 |
| 6 | STT를 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이라는 가짜 구현으로 둔다.
기기는 충전기에 꽂아 둔 전용 태블릿으로 쓰기로 했고, 그래서 배터리 문제는 대부분 사라진다고 봤다.
증상
구현 전에 받은 준비 목록은 다음과 같았다.
microphone타입 Foreground Service + 상시 알림 ("Android 14+에서 타입 명시 필수")- 권한
RECORD_AUDIO,FOREGROUND_SERVICE,FOREGROUND_SERVICE_MICROPHONE,POST_NOTIFICATIONS - 배터리 최적화 예외, 거치 전용기라면
WAKE_LOCK까지 걸면 Doze(화면이 꺼진 기기를 절전시키는 모드) 문제는 "사실상 사라진다" - 부팅 자동 시작은
RECEIVE_BOOT_COMPLETED - STT는 내장
SpeechRecognizer— "무료·로컬. 온디바이스 인식 켜면 오프라인도 가능"
목록만 보면 선언 몇 줄로 끝날 것 같다. 이 목록 그대로 앱을 짜면 부팅 후 녹음이 시작되지 않거나, 오프라인이라고 믿은 인식이 실은 서버로 오디오를 보내는 상황을 만날 수 있다.
원인
목록의 항목들이 "선언하면 된다"와 "언제 시작하느냐에 따라 막힌다"를 구분하지 않았다.
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와 특정 인식 서비스 조합이 항상 실패한다고까지는 단정할 수 없지만, 동시에 된다는 보장도 없다. 오디오 포커스는 출력 중재라 이 문제를 풀어주지 않는다. (오디오 입력 공유)
해결
구현 전에 체크리스트를 이렇게 고쳐 쓴다.
시작 흐름
- 앱 화면(Activity)에서
RECORD_AUDIO런타임 권한을 받는다 → 같은 화면에서 서비스를 시작하고startForeground()에ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE을 넘긴다 - 부팅 후에는 "앱을 한 번 열어야 듣기 시작" 을 기본 동작으로 둔다. 부팅 수신은 알림으로 "눌러서 듣기 시작" 을 안내하는 정도로 쓴다
- 알림은 FGS에 필요하므로 만든다.
POST_NOTIFICATIONS는 받으면 좋지만 없다고 서비스가 안 뜨는 건 아니다
계속 듣기
- 배터리 최적화 예외·충전 거치는 켜 두되, 화면 잠금 상태에서 1시간 → 8시간 순으로 호출어 탐지가 살아 있는지 로그로 잰다. 충전 여부·발열·중단 시각을 같이 남긴다
STT
- 온디바이스 인식기를 명시적으로 만들고, 한국어 모델 상태를 확인한다
- 로컬 필수 모드에서 온디바이스를 못 쓰면 오류로 보여준다. 일반 인식기로 조용히 넘어가면 "오프라인으로 됐다" 는 착각이 생긴다
- 비행기 모드에서 같은 문장들을 인식시켜 성공 수·오류 코드를 따로 집계한다
마이크 전환
- 첫 비교 기준은 순차 전환: 호출어 감지 →
AudioRecord캡처 중지·해제 → STT →MockBrain응답·TTS → 호출어 재개 - STT의 결과·오류·취소 모든 경로에서 호출어 감지로 돌아오게 하고, 재진입을 막는다
- 전환 사이 첫 음절이 잘리는지는 측정 대상으로 남긴다
- API 33의
RecognizerIntent.EXTRA_AUDIO_SOURCE로 이미 열어 둔 오디오를 인식기에 넘기는 방법도 있지만, 지원하지 않는 인식기는 자기 마이크를 연다. 쓰는 인식기가 지원하는지부터 확인한다 (EXTRA_AUDIO_SOURCE)
툴체인
- 이 목록을 확인하려면 먼저 빌드·설치 환경이 있어야 한다. 이번 개발 PC는 JDK 16뿐이라 최신 안드로이드 빌드에 필요한 17 이상을 새로 깔아야 했고,
adb·Android SDK·Android Studio·Gradle이 없었다.
덤 — "거치 전용기라 배터리 걱정 없음"의 범위
충전기에 꽂아 두면 배터리가 닳는 문제는 사라진다. 하지만 이 글의 1~4번 문제(시작 시점 제한, 인식기의 서버 전송, 마이크 공유)는 전원과 무관하다. "거치 전용"은 배터리 항목 하나만 지워 주는 조건으로 읽는 게 안전하다.
취재 후기 — 선언 몇 줄이면 끝날 줄 알았던 목록
*실기기 재현은 아직 없다. 네 사람은 받은 준비 목록을 공식 문서와 한 줄씩 맞춰 봤다.*
RECEIVE_BOOT_COMPLETED. 선언만 하면 전원 켜자마자 듣겠지. RECORD_AUDIO는 앱을 쓰는 동안만 유효한 권한이라, 화면에 앱이 없는 상태에서 마이크 서비스를 새로 만들면 Android 14 조건에서는 SecurityException이 날 수 있대. 게다가 부팅 신호를 받는 수신기는 마이크 타입 서비스를 아예 못 띄운다고 목록에 박혀 있어.SpeechRecognizer, 음성을 글자로 바꿔 주는 안드로이드 기본 부품이잖아. 무료고 기기 안에서 도니까 오프라인도 될 거야. createSpeechRecognizer()는 원격 서버로 오디오를 보낼 수 있다고 적혀 있어. 기기 안에서만 돌리려면 API 31부터 있는 createOnDeviceSpeechRecognizer()를 따로 써야 하고.POST_NOTIFICATIONS도 목록에 있었으니 이것까지 받아야 서비스가 뜨는 거지? 안드로이드 13부터 알림 띄우려면 물어봐야 하잖아. AudioRecord로 마이크를 계속 열고, 인식기도 자기 마이크를 연다며. 둘이 동시에 마이크를 잡으면 한쪽이 무음을 받는 거 아냐?AudioRecord를 놓고, 인식이 끝나거나 실패하면 다시 호출어로 돌아오는 순차 전환으로 가자.adb도 SDK도 Android Studio도 없어.