안드로이드에서는 백그라운드 작업을 돕는 요소들이 있다. 그 요소들 중에서 AlarmManager와 WorkManager를 다뤄 보려고 한다.
AlarmManager는 안드로이드 프레임워크에서 제공하는 API로 시스템 레벨의 알람 서비스에 접근하게 해 준다. 정확한 시간에 대한 작업이 필요한 경우에 사용되며, WorkManager가 없던 시절에는 BroadcastReceiver나 JobScheduler를 함께 사용하여 백그라운드 작업을 수행했다.
WorkManager는 백그라운드에서의 작업을 보장하는 안드로이드 Jetpack 라이브러리이다. 사용자가 화면을 벗어나거나, 앱이 종료가 되더라도 예약한 기능을 안정적으로 수행할 수 있다. 또한 기기의 상태(네트워크 연결, 저장공간, 충전 상태)와 같은 제약 조건을 사용하여 특정 상황에서의 작업 처리도 가능하다.
백그라운드 작업과 관련해서 더 자세한 내용은 아래 페이지를 참고하면 된다.
태스크 예약 | Background work | Android Developers
WorkManager를 사용하여 앱 또는 기기 다시 시작 시 실행되는 작업을 예약하는 방법을 설명합니다.
developer.android.com
WorkManager의 작업은 정해진 시간에 작업을 수행하지 않는다. 시스템이 해당 작업을 수행하기 앞서 효율적인 시점을 선택을 하게 된다. 이런 지연적인 특성은 '정확한 시점의 실행'이 아닌 '언젠가는 반드시 실행'이라는 의미를 갖는다. WorkManager를 사용하되 특정한 시간에 작업을 수행하게 하려면 AlarmManager를 함께 써야 한다.
AlarmManager를 설정하는 버튼을 만들고, 버튼을 클릭 시 WorkManager를 실행한다. WorkManager에서 Room의 값을 업데이트하여 UI에 반영되는지 확인하는 예제를 다음과 같이 만들었다.
◾ 의존성 등록
libs.version.toml
[versions]
lifecycleVersion = "2.9.4"
roomVersion = "2.8.2"
workVersion = "2.10.5"
hiltVersion = "2.57.2"
hiltWorkVersion = "1.3.0"
[libraries]
# ViewModel & LiveData/Flow
lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleVersion" }
lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "lifecycleVersion" }
# Room
room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomVersion" }
room-runtime = { module = "androidx.room:room-runtime", version.ref = "roomVersion" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomVersion" }
# WorkManager
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workVersion" }
# Hilt, HiltPlugin에서 사용 됨 (hilt.android, hilt.compiler)
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hiltVersion" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hiltVersion" }
hilt-work = { group = "androidx.hilt", name = "hilt-work", version.ref = "hiltWorkVersion" }
hilt-android-compiler = { group = "androidx.hilt", name = "hilt-compiler", version.ref = "hiltWorkVersion" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "kspVersion" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hiltVersion" }
[bundles]
room = ["room-ktx", "room-runtime"]
이 프로젝트에서 사용하게 될 라이브러리(work, hilt, room, lifecycle)를 버전 카탈로그에 추가했다. 참고로 Worker 클래스는 Hilt를 사용한 의존성 주입이 가능한데, 이를 위해 hilt-work와 hilt-compiler를 추가한다.
◾ AlarmManager 설정
AlarmManager는 특정 시간에 알람을 켜라고 안드로이드 시스템에 요청한다. 특정 시간이 되면 안드로이드 시스템은 Broadcast를 통해 알람을 울린다.
AlarmScheduler.kt
@Singleton
class AlarmScheduler @Inject constructor(
@ApplicationContext private val context: Context
) {
private val alarmManager = context.getSystemService(Application.ALARM_SERVICE) as AlarmManager
private fun pendingIntent(): PendingIntent {
val intent = Intent(context, AlarmReceiver::class.java)
return PendingIntent.getBroadcast(
context,
ALARM_CODE,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
fun schedule(triggerAtMillis: Long) {
Log.d("LOG_TAG", "AlarmScheduler on")
val pi = pendingIntent()
alarmManager.setAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
triggerAtMillis,
pi
)
}
fun cancel() {
alarmManager.cancel(pendingIntent())
}
}
AlarmScheduler 클래스는 알람을 예약하는 역할을 하는 클래스다.
PendingIntent에 Broadcast를 받을 Class와 알람이 어떻게 동작할지에 대한 플래그를 정의한다.
- PendingIntent.FLAG_UPDATECURRENT
ALARM_CODE로 이미 등록된 PendingIntent가 있을 경우 기존 알람을 취소하지 않고 내용을 현재 Intent로 덮어쓰기(알람 시간 재설정)
- PendingIntent.FLAG_IMMUTABLE
시스템이 이 Intent 내용을 수정할 수 없다고 명시(Android 12 이상부터 필수)
schedule() 함수에서는 알람을 등록하는 역할을 하며, alarmManager.setAndAllowWhileIdle() 함수가 이 기능을 담당한다. triggerAtMillis가 알람이 울릴 정확한 시간이다.(밀리초 단위의 UTC 타임스태프)
- RTC_WAKEUP
RTC(Real Time Clock)은 실제 시간을 의미하고, WAKEUP은 기기가 Doze 모두(깊은 절정 상태)에 빠져 있더라도 강제로 기기를 깨우는 것을 의미한다.
AlarmReceiver.kt
class AlarmReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.d("LOG_TAG", "AlarmReceiver On")
val workManager = WorkManager.getInstance(context.applicationContext)
val workerRequest = OneTimeWorkRequestBuilder<SampleCountWorker>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
workManager.enqueueUniqueWork(
"SampleCountWorkName",
ExistingWorkPolicy.KEEP,
workerRequest
)
}
}
AlarmReceiver 클래스는 알람을 수신하여 동작할 행동을 정의한 클래스다. AlarmScheduler에서 지정한 시간이 되면 AlarmMangaer가 Intent를 통해 AlarmReceiver를 깨운다.
BraodcastReceiver가 알람을 수신하게 되면 onReceive() 함수가 자동으로 호출된다. onReceive 블록에서는 백그라운드 작업을 위해 WorkManager에 위임한다. onReceive()에서 모든 작업을 끝내지 않는 이유는 해당 함수가 메인 스레드에서 실행이 되고, 약 10초가 넘어가면 ANR을 발생시킨다고 한다.
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application
android:name=".SampleApplication"
...
<receiver
android:name=".core.alarm.AlarmReceiver"
android:exported="false" />
</application>
</manifest>
정확한 시간에 알람을 사용하기 위해 SCHEDULE_EXACT_ALARM이라는 특수 권한을 선언한다. 그리고 BroadcastReceiver를 사용하기 때문에 Manifest에 AlarmReceiver를 등록해 준다.
MainViewModel.kt
fun setAlarmManager() {
val timer = 5 // timer(초) 이후 alarmManager 실행
val calendar = Calendar.getInstance()
calendar.add(Calendar.SECOND, timer)
val newHour = calendar.get(Calendar.HOUR_OF_DAY)
val newMinute = calendar.get(Calendar.MINUTE)
val newSecond = calendar.get(Calendar.SECOND)
// 값 업데이트 (ui 업데이트)
_selectedHour.value = newHour
_selectedMinute.value = newMinute
_selectedSecond.value = newSecond
alarmScheduler.cancel() // 기존 알람 제거
alarmScheduler.schedule(triggerAtMillis = calendar.timeInMillis)
viewModelScope.launch {
_toastEvent.emit("${timer}초 뒤에 WorkManager가 실행됩니다.")
}
}
알람 세팅 버튼을 누르면 ViewModel의 setAlarmManager() 함수를 호출하게 했는데, 임시 설정으로 현재 시간에 5초를 더한 시간에 알람을 울리게 하였다.
◾ WorkManager + Hilt로 의존성 주입하기
SampleApplication.kt
@HiltAndroidApp
class SampleApplication : Application(), Configuration.Provider {
@Inject
lateinit var workerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder().setWorkerFactory(workerFactory).build()
}
Hilt가 제공하는 HiltWorkerFactory를 WorkManager의 기본 팩토리로 지정해 준다. 이 설정을 통해 WorkManager가 Hilt로부터 의존성을 주입받은 Worker를 생성할 수 있게 된다.
AndroidManifest.xml
<application
android:name=".SampleApplication"
...
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove"/>
</application>
work-runtime 라이브러리에는 기본적으로 WorkManager를 초기화하는 기능이 추가되어 있어서 앱을 시작하면 InitializationProvider가 자동으로 WorkManager를 기본 설정으로 초기화한다. 그래서 SampleApplication에서 초기화 코드를 작성했음에도 적용이 안 되는 문제가 발생한다. tools:node="remove"를 사용하여 InitializationProvider 자체를 제거하여 자동 초기화를 막아준다.
SampleCountWorker.kt
@HiltWorker
class SampleCountWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val increaseSampleCountUseCase: IncreaseSampleCountUseCase
) : CoroutineWorker(appContext = appContext, params = params) {
override suspend fun doWork(): Result {
try {
Log.d("LOG_TAG", "SampleCountWorker On")
increaseSampleCountUseCase()
} catch (e: Exception) {
Log.e("SampleCountWorker", "Error increasing sample count", e)
}
return Result.success()
}
}
SampleCountWorker는 Worker가 동작하는 클래스다. CoroutineWorker는 WorkManager의 Worker 클래스 중 하나로 작업을 실행하는 함수 doWork()가 suspend 함수가 된다. 이로 인해 비동기 코드를 doWork 블록 내부에서 간단하게 사용할 수 있다.
의존성 주입 관련하여 다음과 같다.
- @HiltWorker
Worker 클래스를 Hilt가 관리함을 알림
- @AssistedInject
(런타임에 사용되는 값 + DI 요소)인 경우에 사용하는 패턴
- @Assited
WorkerManager가 런타임 시 제공할 요소임을 알림
- IncreaseSampleCountUseCase
Application에서 선언한 HiltWorkerFactory가 Hilt 그래프로부터 해당 클래스를 찾아줌

[AlarmManager Set] 버튼을 클릭하면 5초 뒤 AlarmManger를 통해 WorkManager가 실행되고 Room의 Sample 값을 1 증가시킨다. View에서는 Sample 값을 Flow로 수집을 하고 있기 때문에 백그라운드에서 값이 업데이트가 정상적으로 되었는지 확인할 수 있다.
Android_Study/WorkManager with AlarmManager at master · OhGyong/Android_Study
안드로이드 개발 공부. Contribute to OhGyong/Android_Study development by creating an account on GitHub.
github.com
'Android > 기타' 카테고리의 다른 글
| [Android] 인앱 업데이트(in-app update) 적용하기 (0) | 2025.09.17 |
|---|---|
| [Android] Flow와 StateFlow 사용해보기 (0) | 2024.03.26 |