[Android/Jetpack] WorkManager : Background에서 Foreground도

이전에 WorkManager에 대해 2번 포스팅한 적이 있다.
(지금 다시 읽어보니 꽤괜 포스팅...ㅋㅋ)
사실 그때는 그냥 이런게 있구나 정도였는데,
이번 담당 Feature로 펌웨어를 API를 통해 전송하는 기능을 구현하면서 새롭기도 하고 WorkManager를 좀 더 알게된 느낌이다.
그래서 이번 포스팅은 개발하면서 고민했던 부분, 새로워진 구현부에 대해 작성해보려고 한다.
Service
long-running operations을 background에서 수행하는 Application Component
우리가 흔히 아는 Android의 4대 컴포넌트 중 하나라고 할 수 있다.
background에서 돌기 때문에 UI는 제공하지 않고, 다른 앱으로 전환해도 얼마 동안은 계속 실행된다.
Service는 Foreground, Backgroud, Bind 총 3가지 유형이 있다.
Foreground Service
*Notification을 표시해야 하는 Service → 사용자 눈에 보이는 Service
사용자가 작업이 진행되고 있음을 인지할 수 있도록 Notification은 작업이 중단/종료되기 전까지는 사라질 수 없다.
따라서 작업은 Background에서 진행되지만, 사용자에게 UI를 제공함으로서 Foreground에서 표시하는 Service이다.
Background Service
사용자가 직접 알아채지 못하는 작업
말 그대로 Background에서 진행되는 Serivce를 의미한다.
그렇다면 어떤 Component를 사용해야 할까?
🤔 내 상황은 다음과 같았다.
- 3~15분 걸리며 즉시 시작해야 하는 작업
- 진행률과 성공/실패 결과를 Notification으로 표시해야 함
- 앱을 Background에 두더라도 실행이 되어야 함
일단 무조건 Notification을 띄워야 했기 때문에 Foreground Service를 써야 하나 고민했다.
Foreground Service를 사용하기 위해서는 Service가 아닌 Lifecycle-aware한 LifecycleService를 많이 사용하고 있었다.
하지만 결론적으로는 1번만 즉시 실행되는 WorkManager로 구현하게 되었다.
이유는 아래와 같다.
| CoroutineWorker | LifecycleService | |
| 앱 종료 |
|
|
| 결과 표시 (Notificaiton) |
|
|
| 장시간 작업 |
|
|
WorkManager 쉽게 구현하기
- 초기화 하기
- Hilt로 의존성 주입하기
- WorkManager 작성하기
- 상태 관찰하기
비교적 간단한 4가지 단계로 WorkManager를 구현할 수 있다.
1. 초기화 하기
java.lang.IllegalStateException: WorkManager is already initialized.
Application에 WorkManager를 초기화하니 이런 에러가 났다. 처음 선언하는 것인데 이미 초기화가 되었다니?
공식 문서를 다시 읽어보니, WorkManager의 초기화에 대한 3가지 방법이 있었다.
- 앱에서 androidx.startup 라이브러리를 사용하고 있는 경우
<!-- If you want to disable android.startup completely. -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
- androidx.startup 라이브러리를 사용하는 경우
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
- 2.6 이전의 WorkManager를 사용하는 경우
<provider
android:name="androidx.work.impl.WorkManagerInitializer"
android:authorities="${applicationId}.workmanager-init"
tools:node="remove" />
🌐 androidx.startup을 사용한 이유?
WorkManager 내부적으로 2.6.0 버전부터 androidx.startup 라이브러리를 사용하여 초기화해야 함
→ 이전에는 WorkManagerInitializer를 사용해서 초기화하고 있었음
androidx.startup 라이브러리를 간단하게 설명하자면
- android library는 초기화하는 것에 대해 순서를 보장하지 않음
- androidx.startup → android library의 초기화 순서를 정할 수 있음
- 라이브러리마다 사용하는 Content Provider를 초기화하는 비용이 있고, 라이브러리가 많아질 수록 앱 시작 시간이 길어짐
- android.startup → 여러 컴포넌트의 초기화를 하나의 Content Provider를 통해 처리하기 때문에 시간 단축 가능
따라서 앱이 시작할 때 WorkManager를 초기화하는 것이 아니라 해당 컴포넌트가 필요할 때 초기화하고 최적하기 위해 사용한 것.

2. Hilt로 의존성 주입하기
WorkManager 안에서 UseCase, Repository 등을 사용하게 될 경우, Hilt를 사용해야 한다.
이미 hilt를 사용하고 있는 중이라고 가정하고, 아래 hilt-work 라이브러리를 추가한다.
implementation 'androidx.hilt:hilt-work:1.0.0'
@HiltWorker
class TransferWorkManager @AssistedInject constructor(
@Assisted val context: Context,
@Assisted params: WorkerParameters,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
...
}
}
- @HiltWorker : Worker로 사용할 클래스를 정의함
- @AssistedInject : Worker 객체의 생성자를 주입
- @Assisted : 필요한 종속 항목들을 지정
@HiltAndroidApp
class App: Application(), Configuration.Provider {
@Inject
lateinit var transferWorkerFactory: HiltWorkerFactory
override val workManagerConfiguration: Configuration
get() = Configuration.Builder()
.setMinimumLoggingLevel(android.util.Log.DEBUG)
.setWorkerFactory(transferWorkerFactory)
.build()
}
- Hilt에 의해 Worker의 생성자는 HiltWorkerFactory를 통해 생성됨
❇️ HiltWorkerFactory가 아니라 직접 Factory를 만드려면?
class TransferWorkerFactory @Inject constructor(
private val workerParameters: WorkerParameters
): WorkerFactory() {
override fun createWorker(
appContext: Context,
workerParameters: WorkerParameters
): ListenableWorker? {
return when (workerClassName) {
TransferWorkManager::class.java.name -> {
TransferWorkManager(
appContext,
workerParameters
)
}
else -> null
}
}
}
@Module
@InstallIn(SingletonComponent::class)
object WorkerModule {
@Provides
fun provideWorkerFactory(
workerParameters: WorkerParameters
): WorkerFactory {
return TransferWorkerFactory(workerParameters)
}
}
workerParameters에 원하는 UseCase, Repository, Util 클래스 등을 넣을 수 있을 것이다.
3. WorkManager 작성하기
@HiltWorker
class TransferWorkManager @AssistedInject constructor(
@Assisted val context: Context,
@Assisted params: WorkerParameters
) : CoroutineWorker(context, params) {
companion object {
const val WORKER_ID = "transfer_worker"
const val FAIL_MSG = "fail_message"
// input data로 전달받을 value의 key를 선언했음
...
}
override suspend fun doWork(): Result {
// WorkManager가 수행할 작업
...
// 성공
setForeground(createSuccessNotification())
delay(500L)
Result.success()
// 실패
setForeground(createFailedNotification())
delay(500L)
Result.failure(workDataOf(FAIL_MSG to "$errMsg"))
}
}
- WorkManager에는 Request를 Builder로 생성하고 InputData를 넘길 수 있다. (함수의 파라미터와 비슷한 개념)
- 이때 Key-Value형태로 넘기기 때문에 WorkManager 안팎으로 사용하는 Key를 object로 선언해서 사용했다.
- doWork()는 WorkManager가 실제 작동할 로직을 작성한다. (ex. 파일 업로드, 다운로드 등)
fun getTransferWorkRequest(
files: FirmwareFiles,
...
): OneTimeWorkRequest {
val data = Data.Builder()
data.putAll(workDataOf(TransferWorkManager.EMS_FILES to files))
...
return OneTimeWorkRequestBuilder<TransferWorkManager>()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
...
.setInputData(data.build())
.build()
}
- WorkManager로 넘기고자 하는 inputData는 기본형만 지원한다.
- 따라서 복잡한 데이터 구조를 넘기고 싶다면 jsonString으로 파싱해서 WorkManager에 보내고, 받은 뒤에 원하는 데이터 구조로 다시 파싱해서 사용하면 좀 더 깔끔하게 사용할 수 있다.
🏃♀️➡️ 신속 처리 작업
Builder에 보면 setExpedited()를 볼 수 있다. WorkManager 2.7부터 지원하는 기능이다.
할당량이 허용한다면 해당 함수를 사용한 WorkManager는 즉시 백그라운드에서 실행된다.
✅ 그래서 이 할당량에 대한 조건을 설정해주어야 한다.
val request = OneTimeWorkRequestBuilder<SyncWorker>()
<b>.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)</b>
.build()
WorkManager.getInstance(context)
.enqueue(request)
- OutOfQuotaPolicy : 실행하는 앱이 execution quota(= 할당량)에 근접했을 때에 대한 정책
- RUN_AS_NON_EXPEDITED_WORK_REQUEST : ordinary work로 실행하게 함 (신속이 아닌 일반 작업으로 처리)
- DROP_WORK_REQUEST : sufficient quota가 아니라면 cancel (취소)
✅ 한가지 더 주의할 점은, Android 12보다 낮은 플랫폼과의 호환성이다.
더 낮은 플랫폼에서는 WorkManager가 Foreground Service를 실행할 수도 있다.
위에도 표로 정리하여 말했지만 Foreground Service는 Notification을 무조건! 호출해야 한다.
- 12 미만 지원 → getForegroundInfoAsync()나 getForegroundInfo()를 사용해서 Notification 띄우기
- 12 이상 지원 → setForeground()를 통해 Foreground Service 사용
- 그렇지 않으면 Runtime Exception이 발생할 수 있음
실제로 setForegound를 작성하지 않고 일반적인 NotificationBuilder를 통해서 알림을 띄워보니, 앱을 백그라운드로 보냈을 때 WorkManager가 런타임 에러로 인해 재시작되는 비정상 동작이 있었다.
4. 상태(WorkInfo) 관찰하기
workerManger는 LiveData를 지원한다.
우리가 LiveData에서 받아볼 수 있는 데이터는 doWork() (WorkManager가 실행하는 작업)
중간 혹은 완료 후 내보낸 Key-Value 형태의 데이터를 WorkManager의 상태와 함께 WorkInfo라는 데이터 구조로 받을 수 있다.
setProgress(workDataOf(Progress to 0))
Result.success(...)
Result.failure(...)
- setProgress를 통해 진행률을 밖에서 관찰할 수도 있음
- workDataOf를 래핑하고 있는 함수가 곧 WorkInfo의 상태
(아래 상태 말고도 ENQUEUED, CANCELLED, BLOCKED가 있음)- setProgress → State.RUNNING
- Result.success → State.SUCCEEDED
- Result.failure → State.FAILED
WorkManager.getInstance(applicationContext)
// requestId is the WorkRequest id
.getWorkInfoByIdLiveData(requestId)
.observe(observer, Observer { workInfo: WorkInfo? ->
if (workInfo != null) {
val progress = workInfo.progress
val value = progress.getInt(Progress, 0)
// Do something with progress information
}
})
- 앞서 말한 것처럼, Key-Value의 Key를 object로 선언한데는 이처럼 밖에서 Key를 이용해 데이터를 가져와야 한다.
- WorkManager의 ID를 통해서 가져올 수도 있고, WorkManager 자체를 Unique하게 만들 수도 있다.
WorkManager를 공부는 했어도 직접 제대로 구현해본 건 처음이라 글을 남겼다.
내 이전 글이 도움이 되기도 하고, WorkManager는 공식문서가 특히나 자세해서 공식문서만으로도 구현이 쉬웠다.
혹시 잘못된 내용이 있다면 누구든 편하게 댓글 남겨주세요!
Reference
맞춤 WorkManager 구성 및 초기화 | Background work | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 맞춤 WorkManager 구성 및 초기화 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 기본적으로 WorkManager는
developer.android.com
다른 Jetpack 라이브러리와 함께 Hilt 사용 | App architecture | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 다른 Jetpack 라이브러리와 함께 Hilt 사용 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Hilt에는 다른 J
developer.android.com
작업 요청 정의 | Background work | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 작업 요청 정의 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 시작 가이드에서는 WorkRequest를 만들고
developer.android.com
WorkInfo | API reference | Android Developers
androidx.appsearch.builtintypes.properties
developer.android.com
중간 worker 진행률 관찰 | Background work | Android Developers
이 페이지는 Cloud Translation API를 통해 번역되었습니다. 중간 worker 진행률 관찰 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. WorkManager에 worker의 중간 진행률
developer.android.com