안드로이드 WorkManager

ee·2025년 2월 26일

Android

목록 보기
9/10

WorkManager란

WorkManager는 Jetpack Compose에서 제공하는 백그라운드 작업 관리 라이브러리로, 앱을 재실행하거나 시스템이 재부팅될 때 작업이 예약된 채로 남아 있으면 그 작업은 유지된다.

지속적인 작업의 type

WorkManager에서 지속적인 Work의 type은 3가지가 있다.
1. Immediate: 즉시 실행되는 작업
2. Long Running: 지속적으로 실행되는 작업 (잠재적으로 10분 이상)
3. Deferrable: 나중에 실행되고 주기적으로 실행될 수 있는 예약된 작업

특징

Work Constraints를 사용하여 선언적으로 작업의 최적의 조건을 설정한다.
작업은 tag나 name을 주어 unique하게 작업을 예약할 수 있고 그 작업을 대체하거나 취소할 수 있다. 예약된 작업은 내부의 sqlite db에 저장된다. 그리고 자동으로 잠자기 모드에서도 실행된다. 가끔 작업이 실패할 때 flexible retry policies를 제공하여 작업을 재시도할 수 있다.

코드

WorkManager를 사용하여 알림을 설정하는 코드를 짜보았다. 유통기한을 입력받고 유통기한 3일 전, 하루 전, 당일에 알림을 보낸다.

//libs.version.toml
workRuntimeKtx = "2.10.0"
[libraries]
androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "workRuntimeKtx" }

//build.gradle.kts
implementation(libs.androidx.work.runtime.ktx)

먼저 work 라이브러리를 설정해준다.

class Application: Application(){
    override fun onCreate() {
        super.onCreate()
        val name = "유통기한 알림"
        val importance = NotificationManager.IMPORTANCE_HIGH
        val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance)
        val notificationManager: NotificationManager =
            getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.createNotificationChannel(channel)
    }
}

앱이 처음 시작될 때 Notification 채널을 생성해야 한다. 공식문서에서 가장 권장하는 방법이다. 앱이 다시 시작되어 위 코드가 실행 되도 같은 채널 아이디를 가지면 아무 일도 일어나지 않는다.

class ExpirationAlarmWorker(
    context: Context,
    workerParams: WorkerParameters
) : Worker(context, workerParams) {

    override fun doWork(): Result {
        val itemName = inputData.getString("name") ?: "알 수 없음"
        val message = inputData.getString("message") ?: "유통기한 알림"
        showNotification(itemName, message)
        return Result.success()
    }

    private fun showNotification(title: String, message: String) {
        val notificationManager = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // 알림 생성
        val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
            .setContentTitle(title)
            .setContentText(message)
            .setSmallIcon(R.drawable.splash_logo_temp)
            .setAutoCancel(true)
            .build()
        notificationManager.notify(System.currentTimeMillis().toInt(), notification)
    }
}

Worker 클래스를 상속하는 클래스를 생성하고 doWork 함수를 오버라이드한다. 이 함수안에는 작업 요청을 받을 때 실행되고 그에 대한 결과를 반환한다. Intent처럼 inputData를 통해 특정 데이터를 가져올 수 있다.
showNotification 함수에서 알림을 생성하고 보낸다. 이 알림은 전에 생성한 notification channel과 같은 id를 가져야 한다.

fun scheduleExpirationAlarms(context: Context, itemName: String, expirationTime: Long) {
    val currentTime = System.currentTimeMillis()

    // 알람 시간 계산 (3일 전, 1일 전, 당일)
    val alertTimes = listOf(
        expirationTime - TimeUnit.DAYS.toMillis(3), // 3일 전
        expirationTime - TimeUnit.DAYS.toMillis(1), // 1일 전
        expirationTime                               // 당일
    )

    val alertMessages = listOf(
        "$itemName 의 유통기한이 3일 남았습니다.",
        "$itemName 의 유통기한이 하루 남았습니다.",
        "$itemName 의 유통기한이 오늘까지 입니다."
    )

    val workManager = WorkManager.getInstance(context)

    alertTimes.forEachIndexed { index, time ->
        if (time > currentTime) {
            val delay = time - currentTime

            val inputData = Data.Builder()
                .putString("item_name", itemName)
                .putString("message", alertMessages[index])
                .build()

            val workRequest = OneTimeWorkRequestBuilder<ExpirationAlarmWorker>()
                .setInitialDelay(delay, TimeUnit.MILLISECONDS)
                .setInputData(inputData)
                .addTag(itemName)
                .build()

            workManager.enqueue(workRequest)
        }
    }
}

각 알림의 메시지와 시간을 계산하여 리스트를 생성하고 workManager 인스턴스를 가져온다. 각 알림 시간이 현재보다 미래인지를 체크해주고 delay를 계산한다. inputData에는 workManager 라이브러리 중 하나인 Data를 사용하여 이름과 메시지를 입력해준다. 그리고 workRequest 쿼리를 생성하는데 이때 addTag를 주어 이 3개의 알림을 같은 그룹으로 묶어 나중에 취소할 때 tagName을 통하여 한 번에 취소 할 수 있게 한다.

fun cancelExpirationAlarm(context: Context, itemName: String) {
    val workManager = WorkManager.getInstance(context)
    workManager.cancelAllWorkByTag(itemName)
}

같은 tagName을 가진 모든 알림을 취소한다.

				IconButton(
                    onClick = {
                        when {
                            ContextCompat.checkSelfPermission(
                                context,
                                Manifest.permission.POST_NOTIFICATIONS
                            ) == PackageManager.PERMISSION_GRANTED -> {
                                viewModel.toggleNotification(item) // 알림 토글
                                isNotification.value = !isNotification.value
                                if(isNotification.value){
                                    scheduleExpirationAlarms(context, item.name, item.expirationDate)
                                    Log.d("Alarm", "알람 등록")
                                } else {
                                    cancelExpirationAlarm(context, item.name)
                                    Log.d("Alarm", "알람 취소")
                                }
                            }
                            shouldShowRequestPermissionRationale(
                                context as MainActivity, Manifest.permission.POST_NOTIFICATIONS) -> {
                                onShowDialog()
                            }
                            else -> {
                                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                                    requestPermissionLauncher.launch(
                                        Manifest.permission.POST_NOTIFICATIONS)
                                }
                            }
                        }
                    }
                ) {
                    if(isNotification.value){
                        Icon(
                            imageVector = ImageVector.vectorResource(R.drawable.bell_selected),
                            contentDescription = "notification is on",
                            tint = CustomTheme.colors.iconSelected,
                        )
                    } else {
                        Icon(
                            imageVector = ImageVector.vectorResource(R.drawable.bell_outlined),
                            contentDescription = "notification is off",
                            tint = CustomTheme.colors.iconDefault,
                        )
                    }
                }

서버에 저장된 item(name과 expiration date를 가짐)을 가져와서 아이콘 버튼을 클릭할 때 먼저 notification 권한이 있는지 체크하고 있다면 UI에 반영해주고 isNotification 값에 따라 알림을 설정하거나 취소한다. 권한이 없다면 사용자가 권한을 설정하게 유도한다.


앱을 실행하고 권한을 설정하면 유통기한 알림을 보내는 채널이 생성이 된다.

이제 각 아이템에 알림을 설정할 수 있다.

WorkManager를 선택한 이유

전 글에 설명하였듯이 알림을 보내는 데 여러가지 방법이 있는데 WorkManager를 선택한 이유는 각 아이템당 3개의 알림을 설정해야 하는 데 AlarmManager는 시스템 리소스를 사용한다. 만약 아이템이 100개라면 300개의 알림을 생성해야 하는 데 이는 메모리와 배터리가 낭비되기 때문에 그다지 좋은 선택이 아니다. 반면에 WorkManager는 장기적인 작업, 백그라운드에서 실행해야 하는 많은 작업에 적합하며 시스템 리소스를 자동 관리를 해주고 Doze 모드와 배터리 최적화를 지원하고 실패 시 자동으로 재시도 해준다. 또한 앱이 종료되거나 시스템이 재부팅되어도 알림을 유지하는 장점이 있기 때문에 WorkManager를 사용했다.

profile
정진이

0개의 댓글