WorkManager를 사용해보자

HEETAE HEO·2023년 3월 11일
0
post-thumbnail

WorkManager란?

WorkManager는 안드로이드에서 백그라운드 작업을 관리하는 라이브러리입니다.

WorkManager를 사용하면 앱에서 실행되는 작업을 백그라운드에서 안정적으로 실행할 수 있습니다. WorkManager는 안드로이드 API 수준 14이상에서 사용할 수 있으며 이전 버전의 안드로이드에서도 호환성을 제공합니다.

WorkManager를 사용하면 다음과 같은 작업을 수행할 수 있습니다.

WorkManager의 장점

1. 작업 예약: WorkManager를 사용하여 작업을 정확한 시간에 예약할 수 있습니다. 예를 들어 PeriodicWorkRequestBuilder() 를 통해 특정 시간 또는 주기적으로 작업을 실행할 수 있으며 OneTimeWorkRequestBuilder()를 통해 한번만 작업을 수행할 수도 있습니다.

2. 백그라운드 제약 조건: WorkManager는 기기의 배터리 수명과 네트워크 상태등과 같은 백그라운드 제약 조건을 고려하여 작업을 지연시키고, 제약 조건이 충족되면 작업을 실행합니다.

위의 설명을 코드로 보여드리겠습니다.

val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED) // 네트워크 연결 여부 체크
        .setRequiresBatteryNotLow(true) // 배터리 충전 상태 체크
        .setRequiresCharging(true) // 충전 중 체크
        .build()

val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
        .setConstraints(constraints)
        .build()

WorkManager.getInstance(context).enqueue(uploadWorkRequest)

위의 코드에서 Constraints.Builder()로 제약 조건을 설정하고, setRequiredNetworkType(), setRequiresBatteryNotLow(), setRequiresCharging() 등의 메서드를 사용하여 네트워크,배터리 충전 상태, 충전 중의 여부를 확인할 수 있습니다.

3. 다양한 작업 유형의 지원: WorkManager는 한 번 실행되는 단일 작업 뿐만 아니라, 주기적으로 실행되는 작업, 체인으로 연결된 작업 등 다양한 작업 유형을 지원합니다.

이번에도 예시로 설명드리겠습니다.

(1) 한 번 실행되는 단일 작업은 위에서 설명한 OneTimeWorkRequestBuilder()를 통해 만들 수 있습니다.

val downloadWorkRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
        .build()

WorkManager.getInstance(context).enqueue(downloadWorkRequest)

(2) 주기적으로 실행되는 작업

주기적으로 실행되는 작업의 경우 다음과 같이 만들어 줄수있습니다.

val uploadWorkRequest = PeriodicWorkRequestBuilder<DownloadWorker>(15, TimeUnit.MINUTES)
        .build()

WorkManager.getInstance(context).enqueue(uploadWorkRequest)

PeriodicWorkRequestBuilder 클래스를 사용하여 15분 마다 작업을 수행하도록 생성하였습니다.

(3) 체인으로 작업을 연결

WorkManager.getInstance(context)
        .beginWith(downloadWorkRequest)
        .then(uploadWorkRequest)
        .enqueue()

beginWith() 메서드를 사용하여 체인으로 연결된 작업을 수행할 수 있습니다. 가장 처음 실행되는 작업은 downloadWorkRequest이고 then()을 통해 다음 작업을 연결합니다.

작업의 실행이 downloadWorkRequest -> uploadWorkRequest의 순으로 작동하는 것입니다.

4. 작업 결과 : WorkManager는 작업이 성공적으로 완료되거나 실패했을 때 알림을 제공합니다. 이를 통해 작업의 상태를 모니터링하고 문제를 디버깅할 수 있습니다.

위의 같은 특징이 있기 때문에 WorkManager는 안드로이드에서 백그라운드 작업을 안정적으로 관리하기 위한 라이브러리 중 하나입니다.

하지만 단점 또한 존재하게 됩니다.

WorkManager의 단점

1. 작업 실행 시간 제한: WorkManager는 실행 중인 작업에 대해 제한된 시간을 부여하고, 시간 제한을 초과하는 경우 작업이 중단됩니다. 이러한 시간 제한은 네트워크 요청 또는 파일 다운로드와 같은 대규모 작업에 대해 문제가 될 수 있습니다.

2. 외부 종속성: WorkManager는 기기의 배터리 수명과 네트워크 상태 등과 같은 백그라운드 제약 조건을 고려하여 작업을 지연시키고 실행합니다. 이러한 기능을 수행하기 위해서는 Google Play Services와 같은 외부 종속성이 필요합니다.

3. 제한된 제어: WorkManager는 작업 실행의 흐름을 제한하기 때문에 개발자가 작업 실행을 직접 제어할 수 없습니다. 이로 인해 일부 작업에서는 유연성이 부족할 수 있습니다.

4.자원 사용: WorkManager는 백그라운드에서 실행되므로 CPU, 메모리 및 배터리 등의 시스템 자원을 사용합니다. 따라서 작업이 지속적으로 실행될 때, 이러한 자원을 소모하여 다른 앱의 성능에 영향을 미칠 수 있습니다.

5.복잡성: WorkManager는 다양한 작업 유형과 실행 조건을 지원하기 때문에 복잡성이 증가할 수 있습니다. 이로 인해 개발자가 작업을 구성하고 관리하는 데 필요한 노력이 증가할 수 있습니다.

WorkManager의 대체 기술들

우선 WorkManager의 특징을 다시 기술하고 대체 가능한 기술들을 작성해보겠습니다.

workManager는 시스템에 의해 관리되기 때문에 앱이 종료되거나 기기가 재부팅되더라도 작업을 유지하고 작업에 대한 제약 조건을 지정할 수 있습니다. 또한 작업의 실행 순서를 관리하고 병렬 처리 또는 체인으로 순차 처리가 가능합니다. 그렇기에 오래 걸리는 작업이나 사용자의 상호작용이 필요하지 않은 작업에서 실행되니다.

대체 기술

Coroutine은 비동기 프로그래밍 기능으로, 콜백이나 복잡한 스레딩 모델 없이 비동기 작업을 수행하고 작업을 일시 중단 및 재개가 가능합니다. 하지만 작업이 완료되거나 취소될 때까지 앱이 실행되여야 하며 그렇지 않은 경우 작업 손실이 발생할 수 있습니다.

AlarmManager는 Android 시스템 서비스로 애플리케이션에서 특정 시간에 작업을 예약하는데 사용됩니다. 정확한 시간이나 반복 간격에 작업을 실행하는데 사용되며 이벤트 기반 알림이 필요한 경우 사용할 수 있습니다. 절전 모드와 같은 상황에도 작업을 실행할 수 있습니다. -> 시간에 민감한 작업 수행에 중점

WorkManager의 잘못된 사용법

  1. 시간에 민감한 작업에 사용: WorkManager는 시간에 민감하지 않은 작업에 적합하며 만약 정확한 시간에 동작해야하는 작업이면 AlarmManager를 사용해야합니다.

  2. 단일 작업을 위해 사용: WorkManager는 주로 연속적이거나 병렬작업에 적합니다. 간단한 단일 작업의 경우 AsyncTask 또는 Coroutine 등의 다른 메커니즘을 사용하는 것이 더 효율적일 수 있습니다.

  3. UI 작업에 사용: WorkManager는 백그라운드 작업에 적합하며 UI 스레드에서 직접 작업 처리에 사용해서는 안됩니다.

  4. 작업 제약 조건의 설정 부주의: 작업의 실행 조건을 올바르게 설정하지 않으면 작업이 예상대로 동작하지 않거나 리소스를 낭비하게 됩니다. 네트워크가 필요한 작업인데 네트워크 조건을 설정하지 않는다면 네트워크 연결이 되어 있지않는 상황인데도 WorkManager는 동작을 시도할 수 있습니다.

  5. 작업 중복 실행: WorkManager를 사용할 때 작업의 중복 실행을 피해야 합니다. 이를 위해 기존의 작업이 완료되거나 취소가 된 후 새 작업을 스케줄링 하거나, 고유한 작업 이름을 사용하여 중복 실행을 방지해야합니다.

  6. 작업 취소: WorkManager에서 작업을 취소하려면 작업 중 onCancel() 콜백이 호출해야하는데 취소를 잘못하게되면 작업이 계속 실행되거나 리소스가 해제되지 않을 수 있습니다.

WorkManager 사용 코드 설명

저도 WorkManager에 대한 개념만 알고 있었을 뿐 실제 사용해본 적은 없었습니다. 그렇기에 클론 코딩을 통해 WorkManager 코드를 작성하여 이해할 수 있도록 하였습니다.

클론코딩은 Android 유튜버인 Philipp Lackner의 WorkManagerGuide 입니다.

DownloadWorker

이 클래스는 WorkManager에서 사용되는 CoroutineWorker를 확장해 이미지 파일을 다운로드 하고 캐시 디렉터리에 저장한 후 결과를 반환하도록 도와주는 클래스 파일입니다.

class DownloadWorker(
    private val context: Context,
    private val workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    override suspend fun doWork(): Result {
        startForegroundService()
        delay(5000L)
        val response = FileApi.instance.downloadImage()
        response.body()?.let { body ->
            return withContext(Dispatchers.IO) {
                val file = File(context.cacheDir, "image.jpg")
                val outputStream = FileOutputStream(file)
                outputStream.use { stream ->
                    try {
                        stream.write(body.bytes())
                    } catch (e: IOException) {
                      return@withContext Result.failure(
                          workDataOf(
                              WorkerKeys.ERROR_MSG to e.localizedMessage
                          )
                      )
                    }
                }
                Result.success(
                    workDataOf(
                        WorkerKeys.IMAGE_URI to file.toUri().toString()
                    )
                )
            }
        }
        if (!response.isSuccessful){
            if(response.code().toString().startsWith("5")){
                return Result.retry()
            }
            return Result.failure(
                workDataOf(
                    WorkerKeys.ERROR_MSG to "Network error"
                )
            )
        }
        return Result.failure(
            workDataOf(
                WorkerKeys.ERROR_MSG to "Unknown error"
            )
        )
    }
}

한번에 모든 코드를 작성하고 설명하는 것은 복잡할 것 같아 나눠서 설명하겠습니다.

doWork()에서는 메서드의 실제 작업이 이루어지는 곳 입니다. 해당 작업을 설명하 면 startForegroundService()를 호출하여 포그라운드 서비스를 시작합니다. delay()의 경우에는 작업의 수행 텀을 주기 위해 인위적으로 넣어준 것으로 5초 뒤 이미지가 다운로드 됩니다.

5초 뒤 이미지가 다운로드 되고 다운로드가 성공이 되면 캐시 디렉터리에 저장한 후 결과로 이미지 URI를 반환하게 됩니다. 만약 실패하게 될때 5로 시작하는 상태 코드이면 재시도를 하게 되고 다른 코드라면 실패라는 메시지를 반환하게 됩니다.

    private suspend fun startForegroundService() {
        setForeground(
            ForegroundInfo(
                Random.nextInt(),
                NotificationCompat.Builder(context, "download_channel")
                    .setSmallIcon(R.drawable.ic_launcher_background)
                    .setContentText("Downloading....")
                    .setContentTitle("Download in progress")
                    .build()
            )
        )
    }

startForegroundService() 는 작업이 포그라운드에서 실행되도록 설정하는 메서드입니다. 이렇게 하면 작업이 실행중인 동안 사용자에게 알림을 표시하여 앱의 상태를 알릴 수 있습니다.

ColorFilterWorker

해당 클래스 파일에서는 이미지 파일에 색상 필터를 적용하고 캐시 디렉터리에 저장한 후 결과를 반환하는 역할을 수행합니다.

코드

class ColorFilterWorker(
    private val context: Context,
    private val workerParams: WorkerParameters
): CoroutineWorker(context,workerParams) {

    override suspend fun doWork(): Result {
        val imageFile = workerParams.inputData.getString(WorkerKeys.IMAGE_URI)
            ?.toUri()
            ?.toFile()
        delay(5000L)
        return imageFile?.let { file ->
            val bmp = BitmapFactory.decodeFile(file.absolutePath)
            val resultBmp = bmp.copy(bmp.config,true)
            val paint = Paint()
            paint.colorFilter = LightingColorFilter(0x08FF04, 1)
            val canvas = Canvas(resultBmp)
            canvas.drawBitmap(resultBmp,0f,0f,paint)

            withContext(Dispatchers.IO){
                val resultImageFile = File(context.cacheDir,"new-image.jpg")
                val outputStream = FileOutputStream(resultImageFile)
                val successful = resultBmp.compress(
                    Bitmap.CompressFormat.JPEG,
                    90,
                    outputStream
                )
                if(successful){
                    Result.success(
                        workDataOf(
                            WorkerKeys.FILTER_URI to resultImageFile.toUri().toString()
                        )
                    )
                } else Result.failure()
            }
        }?: Result.failure()
    }
}

doWork()를 설명하면 입력 데이터로 부터 이미지 파일의 URI를 가져오고 이미지파일이 존재한다면 비트맵으로 로드한 뒤 새 비트맵을 만들어줍니다. 새 비트맵에 색상필터를 적용하고 적용한 파일을 캐시 디렉터리에 저장한 뒤 작업의 결과를 새 이미지 파일의 URI로 반환하게 됩니다.

MainActivity

class MainActivity : ComponentActivity() {
    @SuppressLint("UnrememberedMutableState")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val downloadRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
            .setConstraints(
                Constraints.Builder()
                    .setRequiredNetworkType(
                        NetworkType.CONNECTED
                    )
                    .build()
            )
            .build()

        val colorFilterRequest = OneTimeWorkRequestBuilder<ColorFilterWorker>()
            .build()
        val workManager = WorkManager.getInstance(applicationContext)



        setContent {
            WorkManagerTestTheme {
                val workInfos = workManager
                    .getWorkInfosForUniqueWorkLiveData("download")
                    .observeAsState()
                    .value
                val downloadInfo = remember(key1 = workInfos) {
                    workInfos?.find { it.id == downloadRequest.id }
                }
                val filterInfo = remember(key1 = workInfos) {
                    workInfos?.find { it.id == colorFilterRequest.id }
                }
                val imageUri by derivedStateOf {
                    val downloadUri = downloadInfo?.outputData?.getString(WorkerKeys.IMAGE_URI)
                        ?.toUri()
                    val filterUri = filterInfo?.outputData?.getString(WorkerKeys.FILTER_URI)
                        ?.toUri()
                    filterUri ?: downloadUri
                }
                Column(
                    modifier = Modifier.fillMaxSize(),
                    verticalArrangement = Arrangement.Center,
                    horizontalAlignment = Alignment.CenterHorizontally
                ) {
                    imageUri?.let { uri ->
                        Image(
                            painter = rememberImagePainter(
                                data = uri
                            ),
                            contentDescription = null,
                            modifier = Modifier.fillMaxWidth()
                        )
                        Spacer(modifier = Modifier.height(16.dp))
                    }
                    Button(
                        onClick = {
                            workManager
                                .beginUniqueWork(
                                    "download",
                                    ExistingWorkPolicy.KEEP,
                                    downloadRequest
                                )
                                .then(colorFilterRequest)
                                .enqueue()
                        },
                        enabled = downloadInfo?.state != RUNNING
                    ) {
                        Text(text = "Start download")
                    }
                    Spacer(modifier = Modifier.height(8.dp))
                    when(downloadInfo?.state) {
                        RUNNING -> Text("Downloading...")
                        SUCCEEDED -> Text("Download succeeded")
                        FAILED -> Text("Download failed")
                        CANCELLED -> Text("Download cancelled")
                        ENQUEUED -> Text("Download enqueued")
                        BLOCKED -> Text("Download blocked")
                        null ->  Text("")
                    }
                    Spacer(modifier = Modifier.height(8.dp))
                    when(filterInfo?.state) {
                        RUNNING -> Text("Applying filter...")
                        SUCCEEDED -> Text("Filter succeeded")
                        FAILED -> Text("Filter failed")
                        CANCELLED -> Text("Filter cancelled")
                        ENQUEUED -> Text("Filter enqueued")
                        BLOCKED -> Text("Filter blocked")
                        null ->  Text("")
                    }
                }
            }
        }
    }
}
  • onCreate() : 해당 메서드에서는 DownloadWorker와 ColorFilterWorker를 실행할 작업 요청을 생성하고 WorkManager 인스턴스를 가져오빈다.

  • setContent() : 해당 메서드에서는 LiveData를 사용하여 작업 상태를 관찰하고, 이미지가 있다면 이미지 URI를 가져와 화면에 표시해줍니다.

Start download 버튼을 클릭하게 된다면 DownloadWorker와 ColorFilterWorker를 순차적으로 실행하게되면 상태에 따른 메시지를 출력하게 해줍니다.

다음과 같이 사용하여 Jetpack Compose에서의 WorkManager를 사용하는 방법에 대해 간단하게 알게 되었습니다. 저는 단순히 클론 코딩을 했다고 해당 기능에 대해서 이해했다고 생각하지 않습니다. 이는 단순한 암기와 같은 방법이기에 다른 조건이 추가되거나 한다면 전혀 사용하지 못할 것이기 때문입니다.

그렇기에 클론 프로젝트를 바탕으로 리팩토링을 계획할 생각입니다. 만약 리팩토링을 하게 된다면 추가적으로 글을 작성하도록 하겠습니다.

References

https://www.youtube.com/watch?v=Psc2xyutE2U

profile
Android 개발 잘하고 싶어요!!!

0개의 댓글