이 글은 누구를 위한 것인가
- 백그라운드 작업 때문에 배터리 소모가 크다는 리뷰를 받는 팀
- WorkManager vs JobScheduler vs AlarmManager 중 무엇을 써야 할지 모르는 개발자
- Doze 모드에서 작업이 실행되지 않는 문제를 겪는 엔지니어
들어가며
Android 6.0 이후 백그라운드 작업 제한이 계속 강화됐다. "알림이 안 온다", "동기화가 안 된다"는 리뷰는 대부분 Doze 모드와 백그라운드 제한 때문이다.
이 글은 bluefoxdev.kr의 Android 백그라운드 처리 가이드 를 참고하여 작성했습니다.
1. Android 백그라운드 제한 이해
[백그라운드 제한 타임라인]
Android 6.0: Doze 모드, App Standby
Android 7.0: Doze-on-the-Go (이동 중에도)
Android 8.0: 백그라운드 서비스 제한
Android 9.0: 백그라운드 앱 네트워크 제한
Android 12+: 정확한 알람 권한 제한
[Doze 모드 진입 조건]
화면 꺼짐 + 충전 안 됨 + 정지 상태
→ Light Doze (즉시)
→ Full Doze (몇 시간 후)
[Doze 중 불가능한 것]
네트워크 접근
Wake lock
AlarmManager (정확한 알람)
JobScheduler 신규 시작
[Doze 중 가능한 것]
FCM 고우선순위 푸시 (예외)
유지보수 윈도우 (Doze 해제 시 잠깐)
Foreground Service (알림 있어야)
[백그라운드 작업 선택 기준]
즉시 실행 + 사용자에게 보임: Foreground Service
즉시 실행 + 백그라운드: Thread/Coroutine (앱 실행 중만)
지연 가능 + 보장 필요: WorkManager ✓
정확한 시간: AlarmManager (권한 필요)
2. WorkManager 최적화 구현
import android.content.Context
import androidx.work.*
import java.util.concurrent.TimeUnit
// 백그라운드 동기화 Worker
class DataSyncWorker(
context: Context,
params: WorkerParameters,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
val data = inputData.getString("sync_type") ?: "full"
// 네트워크 상태 확인 (Constraints로 보장되지만 이중 체크)
if (!isNetworkAvailable()) {
return Result.retry()
}
performSync(data)
Result.success(
workDataOf("sync_time" to System.currentTimeMillis())
)
} catch (e: Exception) {
if (runAttemptCount < 3) Result.retry()
else Result.failure()
}
}
private suspend fun performSync(type: String) {
// 실제 동기화 로직
}
private fun isNetworkAvailable(): Boolean {
val cm = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE)
as android.net.ConnectivityManager
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET)
}
}
// WorkManager 설정
object WorkScheduler {
fun schedulePeriodicSync(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val syncRequest = PeriodicWorkRequestBuilder<DataSyncWorker>(
repeatInterval = 15,
repeatIntervalTimeUnit = TimeUnit.MINUTES,
)
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS,
)
.setInputData(workDataOf("sync_type" to "incremental"))
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"data_sync",
ExistingPeriodicWorkPolicy.KEEP, // 기존 작업 유지
syncRequest,
)
}
fun scheduleOneTimeUpload(context: Context, fileUri: String) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(constraints)
.setInputData(workDataOf("file_uri" to fileUri))
.addTag("upload")
.build()
WorkManager.getInstance(context).enqueue(uploadRequest)
}
// 체인 작업
fun scheduleChainedWork(context: Context) {
val compress = OneTimeWorkRequestBuilder<CompressWorker>().build()
val upload = OneTimeWorkRequestBuilder<UploadWorker>().build()
val notify = OneTimeWorkRequestBuilder<NotifyWorker>().build()
WorkManager.getInstance(context)
.beginWith(compress)
.then(upload)
.then(notify)
.enqueue()
}
}
// Foreground Service (사용자에게 보이는 장기 작업)
class DownloadForegroundService : android.app.Service() {
private val NOTIFICATION_ID = 1001
private val CHANNEL_ID = "download_channel"
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
createNotificationChannel()
val notification = buildProgressNotification(0)
startForeground(NOTIFICATION_ID, notification)
// 실제 작업 (코루틴으로)
kotlinx.coroutines.CoroutineScope(kotlinx.coroutines.Dispatchers.IO).launch {
performDownload()
stopSelf()
}
return START_NOT_STICKY
}
private fun buildProgressNotification(progress: Int): android.app.Notification {
return androidx.core.app.NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("다운로드 중...")
.setProgress(100, progress, progress == 0)
.setSmallIcon(android.R.drawable.stat_sys_download)
.setOngoing(true)
.build()
}
private fun createNotificationChannel() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
val channel = android.app.NotificationChannel(
CHANNEL_ID,
"다운로드",
android.app.NotificationManager.IMPORTANCE_LOW,
)
getSystemService(android.app.NotificationManager::class.java)
.createNotificationChannel(channel)
}
}
override fun onBind(intent: Intent?) = null
private suspend fun performDownload() { /* 다운로드 로직 */ }
}
마무리
백그라운드 작업의 기본 원칙: 지연 가능한 작업은 WorkManager, 사용자가 알아야 하는 장기 작업은 Foreground Service. AlarmManager는 정확한 시간의 알람에만 쓰고 반드시 권한을 요청하라. 배터리 최적화 예외(REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)는 꼭 필요한 앱(헬스케어, 보안)만 써야 Google Play 정책을 통과한다.