[안드로이드 17] 백그라운드 오디오 제한: 사용자가 의도하지 않은 백그라운드 오디오 재생이나 볼륨 제어에 대한 제약이 강화에 대한 대응 방향( 샘플 코드)
핵심은 “사용자가 지금 이 앱이 소리를 내고 있다는 사실을 명확히 인지하고 있는가?”를 시스템이 더 엄격하게 따지겠다는 것입니다.
안드로이드 17부터 적용되는 백그라운드 오디오 및 포그라운드 서비스(FGS) 제약 사항의 핵심 내용을 정리해 드립니다.
- 포그라운드 서비스(FGS) 유형 준수 강화
이전 버전까지는 mediaPlayback 유형의 서비스를 선언만 하면 백그라운드에서 오디오를 재생하는 데 큰 제약이 없었습니다. 하지만 안드로이드 17부터는 다음과 같은 조건이 엄격해집니다.
시스템 가시성: 앱이 백그라운드에 있을 때 오디오를 재생하려면 반드시 mediaPlayback 유형의 포그라운드 서비스를 실행해야 합니다.
활성 세션 필수: 서비스만 떠 있는 게 아니라, 실제로 MediaSession이 활성화되어 있어야 하며 시스템 미디어 컨트롤(알림창의 재생/정지 버튼)에 앱의 상태가 표시되어야 합니다.
- 오디오 포커스(Audio Focus) 획득 제약
가장 큰 변화 중 하나는 앱이 백그라운드에 있는 상태에서 ‘처음으로’ 오디오 포커스를 요청할 때 발생합니다.
시나리오: 앱이 화면에 보이지 않는 상태(Background)인데 갑자기 소리를 재생하려고 requestAudioFocus()를 호출하는 경우.
안드로이드 17의 동작: 시스템은 이 요청을 거절하거나, 사용자가 명시적으로 허용한 경우에만 승인합니다.
예외: 사용자가 최근에 앱과 상호작용했거나(짧은 시간 내), 블루투스 헤드셋의 재생 버튼을 누르는 등의 ‘사용자 의도’가 명확할 때만 허용됩니다.
- 볼륨 제어 및 가로채기 방지
사용자가 다른 앱을 사용 중일 때 백그라운드 앱이 시스템 볼륨을 갑자기 키우거나 제어하는 행위가 제한됩니다.
원치 않는 소음 방지: 앱이 포그라운드에 있지 않을 때 AudioManager를 통해 스트림 볼륨을 직접 변경하려고 하면 시스템에 의해 무시될 수 있습니다.
사용자 제어권 강화: 사용자가 시스템 설정에서 특정 앱의 백그라운드 오디오 재생 권한을 더 쉽게 박탈할 수 있는 옵션이 추가됩니다.
- 개발자가 주의해야 할 점 (Technical Check)
공유해주신 문서의 핵심 가이드라인은 다음과 같습니다.
MediaSession 사용 필수: 단순히 MediaPlayer나 ExoPlayer만 돌리는 게 아니라, MediaSessionCompat 등을 통해 시스템에 “우리가 지금 미디어를 재생 중이다”라는 신호를 명확히 보내야 합니다.
서비스 실행 시점: 앱이 백그라운드로 넘어가기 전(사용자가 화면을 보고 있을 때)에 포그라운드 서비스를 시작해야 합니다. 백그라운드에 진입한 ‘후’에 서비스를 시작하려고 하면 ForegroundServiceStartNotAllowedException이 발생할 확률이 매우 높습니다.
오디오 포커스 결과 처리: 이제 requestAudioFocus()가 항상 AUDIOFOCUS_REQUEST_GRANTED를 반환할 것이라고 가정하면 안 됩니다. AUDIOFOCUS_REQUEST_FAILED가 반환되었을 때의 예외 처리를 반드시 구현해야 합니다.
요약
만약 개발 중이신 앱 중에 알람 기능이나 백그라운드 알림음 송출 기능이 있다면, 안드로이드 17 환경에서 “화면이 꺼진 상태나 다른 앱 사용 중에 소리가 정상적으로 트리거되는지”를 반드시 확인해야 합니다.
특히 일반적인 음악 재생 앱이 아니라 특정 조건(예: 배터리 충전 알림 등)에서 짧은 효과음을 내야 하는 경우라면, MediaPlayer보다는 Notification의 소리 설정을 이용하거나, 시스템이 권장하는 WorkManager + Foreground Service 조합을 정확히 사용하고 있는지 점검해 보시기 바랍니다.
영향을 받는 오디오 api 전체 목록 및 풀이
안드로이드 17에서 백그라운드 오디오 제한 정책을 위반했을 때, 개발자가 사용하는 API들이 실제로 어떻게 작동(혹은 실패)하는지를 명시한 표입니다. 가장 무서운 점은 앱이 실행 중임에도 불구하고 “에러 메시지 없이 그냥 무시된다”는 것입니다.

- 오디오 재생 (Audio Playback)
앱이 백그라운드에서 몰래 소리를 내려고 할 때의 동작입니다.
결과: 재생이 무음 처리(Muted)됩니다.
특이사항: 시스템은 예외(Exception)를 던지지 않으며, API 레벨에서 별도의 실패 메시지도 제공하지 않습니다. 즉, 코드는 정상적으로 실행되는 것처럼 보이지만 사용자는 아무 소리도 듣지 못합니다.
영향을 받는 API: AudioTrack.write(), NDK의 AAudioStream_write, OpenSL ES 등 직접 오디오 데이터를 쓰는 API들입니다. Exoplayer나 media3 같은 라이브러리도 결국 이 하위 API를 쓰기 때문에 똑같이 영향을 받습니다.
- 오디오 포커스 요청 (Audio Focus Request)
소리를 내기 위해 시스템에 “나 지금 소리 좀 내도 될까?”라고 물어보는 과정입니다.
결과: AUDIOFOCUS_REQUEST_FAILED라는 값을 반환하며 거절당합니다.
특이사항: 현재 소리를 재생 중인 다른 앱(예: 유튜브, 멜론 등)에 아무런 영향을 주지 못하며, 포커스를 뺏어올 수도 없습니다.
해당 API: AudioManager.requestAudioFocus().
- 볼륨 및 벨소리 모드 API (Volume & Ringer Mode)
백그라운드에서 시스템 설정(소리 크기, 진동 모드 등)을 바꾸려고 할 때입니다.
결과: 메서드 호출이 자동으로 무시됩니다. 벨소리 모드나 볼륨 크기에 아무런 변화가 생기지 않습니다.
특이사항: 이 역시 에러나 예외 발생 없이 그냥 ‘조용히’ 무시됩니다. 개발자 입장에서는 코드가 왜 안 먹히는지 로그만 봐서는 알기 어려울 수 있습니다.
해당 API: setStreamVolume(), setStreamMute(), setRingerMode() 등 볼륨과 관련된 거의 모든 AudioManager 메서드입니다
MediaSession 연동 예제 코드
MediaPlayer를 사용해 노래를 재생할 때, MediaSession을 생성하여 서비스와 연결하는 예제 코드입니다. 안드로이드 17의 새로운 정책에 대응하기 위해서는 단순히 소리를 재생하는 것을 넘어, 시스템이 관리하는 미디어 세션에 앱의 재생 상태를 등록하는 것이 필수적입니다.
MediaSession을 연동하면 시스템 미디어 컨트롤러(알림창 재생 제어)와 연결되어, 앱이 백그라운드에 있더라도 “정당한 미디어 재생”으로 인정받아 무음 처리를 피할 수 있습니다.
다음은 포그라운드 서비스 내에서 MediaPlayer와 MediaSession을 결합하여 사용하는 예제 코드입니다.
포그라운드 서비스 예제 코드 (Kotlin)
class AlarmService : Service() {
private var mediaPlayer: MediaPlayer? = null
private var mediaSession: MediaSession? = null
override fun onCreate() {
super.onCreate()
// 1. MediaSession 초기화
mediaSession = MediaSession(this, "ChargingAlarmService").apply {
// 시스템 미디어 버튼 및 콜백 설정
setCallback(object : MediaSession.Callback() {
override fun onPlay() { mediaPlayer?.start() }
override fun onPause() { mediaPlayer?.pause() }
override fun onStop() { stopSelf() }
})
isActive = true
}
// 2. 포그라운드 서비스 시작 (안드로이드 14+ 대응)
startForeground(NOTIFICATION_ID, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
}
private fun playAlarmMusic() {
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer.create(this, R.raw.alarm_music).apply {
isLooping = true
}
}
// 3. 재생 상태를 시스템에 보고
val state = PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PLAY or PlaybackState.ACTION_PAUSE or PlaybackState.ACTION_STOP)
.setState(PlaybackState.STATE_PLAYING, 0, 1.0f)
.build()
mediaSession?.setPlaybackState(state)
mediaPlayer?.start()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent?.action == "ACTION") {
playAlarmMusic()
}
return START_STICKY
}
override fun onDestroy() {
mediaPlayer?.release()
mediaSession?.apply {
isActive = false
release()
}
super.onDestroy()
}
override fun onBind(intent: Intent?) = null
private fun createNotification(): Notification {
// MediaStyle 알림 생성 (중요)
return Notification.Builder(this, CHANNEL_ID)
.setContentTitle("알림")
.setContentText("상태를 감시 중입니다.")
.setSmallIcon(R.drawable.ic_battery_full)
.setStyle(Notification.MediaStyle().setMediaSession(mediaSession?.sessionToken))
.build()
}
}
포그라운드 예제 코드(JAVA) : AlarmService.java (Java 예제)
import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.content.pm.ServiceInfo;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.IBinder;
public class AlarmService extends Service {
private MediaPlayer mediaPlayer;
private MediaSession mediaSession;
private AudioManager audioManager;
@Override
public void onCreate() {
super.onCreate();
audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
// 1. MediaSession 초기화
mediaSession = new MediaSession(this, "ChargingAlarmService");
mediaSession.setCallback(new MediaSession.Callback() {
@Override
public void onPlay() {
playAlarmMusic();
}
@Override
public void onStop() {
stopSelf();
}
});
mediaSession.setActive(true);
// 2. 안드로이드 14+ 대응: 포그라운드 서비스 시작 시 타입 명시
// createNotification()은 알림 채널 설정이 완료된 Notification 객체를 반환해야 합니다.
startForeground(1001, createNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK);
}
private void playAlarmMusic() {
// 3. 오디오 포커스 요청 (안드로이드 17 대응 핵심)
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) stopSelf();
})
.build();
int res = audioManager.requestAudioFocus(focusRequest);
// 포커스를 얻었거나 대기 중일 때만 재생 진행
if (res == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer.create(this, R.raw.charging_complete_song);
mediaPlayer.setLooping(true);
}
// 4. PlaybackState 업데이트 (시스템에 재생 중임을 알림)
PlaybackState state = new PlaybackState.Builder()
.setActions(PlaybackState.ACTION_PLAY | PlaybackState.ACTION_PAUSE | PlaybackState.ACTION_STOP)
.setState(PlaybackState.STATE_PLAYING, 0, 1.0f)
.build();
mediaSession.setPlaybackState(state);
mediaPlayer.start();
} else {
// AUDIOFOCUS_REQUEST_FAILED 대응
// 소리를 낼 수 없으므로 진동이나 다른 알림 수단 활용 권장
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (intent != null && "ACTION_PLAY_ALARM".equals(intent.getAction())) {
playAlarmMusic();
}
return START_STICKY;
}
private Notification createNotification() {
// 반드시 Notification.MediaStyle을 사용하여 MediaSession과 연결해야 합니다.
return new Notification.Builder(this, "YOUR_CHANNEL_ID")
.setContentTitle("알림 작동 중")
.setContentText("노래가 재생됩니다.")
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setStyle(new Notification.MediaStyle()
.setMediaSession(mediaSession.getSessionToken()))
.build();
}
@Override
public void onDestroy() {
if (mediaPlayer != null) {
mediaPlayer.release();
mediaPlayer = null;
}
if (mediaSession != null) {
mediaSession.setActive(false);
mediaSession.release();
}
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) { return null; }
}
핵심 구현 포인트
MediaSession활성화:isActive = true를 설정해야 시스템이 이 세션을 인식합니다.PlaybackState업데이트: 단순히 노래만 트는 것이 아니라,setPlaybackState()를 통해 현재 앱이STATE_PLAYING상태임을 시스템에 알려야 합니다.Notification.MediaStyle: 알림을 만들 때.setStyle(Notification.MediaStyle())을 사용하여MediaSession의 토큰을 연결하세요. 이렇게 하면 잠금 화면이나 알림창에서 사용자가 직접 제어할 수 있게 되어 시스템의 의심(?)을 피할 수 있습니다.- 서비스 타입 명시:
AndroidManifest.xml에서 서비스 선언 시foregroundServiceType="mediaPlayback"이 반드시 포함되어야 하며,startForeground()호출 시에도 해당 타입을 전달해야 합니다. requestAudioFocus결과 체크:AUDIOFOCUS_REQUEST_FAILED가 반환되면 안드로이드 17 시스템이 재생을 차단한 것이므로, 예외 처리가 반드시 필요합니다.PlaybackState.STATE_PLAYING설정: 이 상태값이 설정되어야 시스템이 “사용자가 인지하는 재생”으로 간주하여AudioTrack.write()의 무음 처리를 방지합니다.Notification.MediaStyle: 알림에 미디어 스타일을 적용하고setMediaSession을 호출해야 사용자가 알림창에서 재생을 제어할 수 있고, 시스템도 이를 정당한 미디어 앱으로 화이트리스트 처리합니다.
추가 팁: 오디오 포커스
노래를 재생하기 직전에 AudioManager.requestAudioFocus()를 호출하여 포커스를 요청하는 로직도 함께 포함하는 것이 좋습니다. 만약 안드로이드 17 기기에서 포커스 요청이 거절(AUDIOFOCUS_REQUEST_FAILED)된다면, 무리하게 재생하기보다는 사용자에게 알림 메시지를 띄우는 처리가 필요합니다.
<manifest ...>
<!-- 1. 포그라운드 미디어 재생 권한 추가 (필수) -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<application ...>
<service
android:name=".ManageService"
android:exported="false"
android:foregroundServiceType="mediaPlayback" />
</application>
</manifest>



