본문 바로가기
ANDROID/Android Jetpack

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

by 주 녕 2025. 4. 19.
반응형

https://developer.android.com/

 

이전에 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
앱 종료
  • 앱이 종료되어도 파일 전송은 지속됨 (성공 확률 ↑)
  • LifecycleService는 Activity, Fragment와 같은 LifecycleOwner와 연결되어, 그들의 Lifecycle에 따라 동작됨
  • 따라서 앱 종료 시, 함께 종료 (성공 확률 ↓)
결과 표시
(Notificaiton)
  • 전송 완료 시, Result를 통해 결과 전달
  • Foreground Serviced와 Notification 표시 가능
  • 필수로 Notification을 표시해야 함
  • 상태 관찰 기능 없음
장시간 작업
  • 장시간 작업 지원
  • 실패 시 재시도하도록 설정 가능
  • 자동 재시도 기능은 없음



 


 

 

 

WorkManager 쉽게 구현하기

  1. 초기화 하기
  2. Hilt로 의존성 주입하기
  3. WorkManager 작성하기
  4. 상태 관찰하기

비교적 간단한 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,
    private val localUseCase: LocalUseCase,
    private val localFwUseCase: EmbLocalFirmwareUpdateUseCase,
    private val networkUtil: NetworkUtil,
) : 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

 

반응형

댓글