Android WorkManager 2026 완벽 가이드: 백그라운드 작업, 제약 조건, 면접 질문

Android WorkManager를 활용한 백그라운드 작업 처리 방법을 상세히 설명합니다. 제약 조건 설정, 주기적 실행, 체인 처리, 2026년 기술 면접에서 자주 출제되는 질문까지 포괄적으로 다룹니다.

Android WorkManager를 활용한 백그라운드 작업 처리 다이어그램

2026년 현재, Android WorkManager는 백그라운드 작업 처리를 위한 표준 솔루션으로 확립되었습니다. Android 15의 배터리 최적화 강화로 인해 앱이 백그라운드에서 안정적으로 작업을 실행하는 것이 그 어느 때보다 중요해졌습니다. WorkManager는 앱이 종료되거나 기기가 재시작된 후에도 지연 가능하고 신뢰성 있는 비동기 작업을 실행하기 위한 Android Jetpack 라이브러리입니다. 이 글에서는 WorkManager의 기본 개념부터 고급 사용법, 그리고 2026년 기술 면접에서 자주 출제되는 질문까지 상세히 다룹니다.

WorkManager를 선택해야 하는 이유

WorkManager는 JobScheduler, AlarmManager, Firebase JobDispatcher의 기능을 통합하여 API 레벨 21 이상에서 일관된 동작을 보장합니다. 작업의 제약 조건(네트워크 연결, 충전 상태 등)을 선언적으로 지정할 수 있으며, 시스템이 작업의 최적 실행 시점을 자동으로 결정합니다. Foreground Service가 즉시 실행이 필요한 작업용인 반면, WorkManager는 지연 가능하지만 반드시 실행되어야 하는 작업에 최적화되어 있습니다.

WorkManager 기초: OneTimeWorkRequest와 Worker 클래스

WorkManager의 기본 구성 요소는 Worker 클래스와 WorkRequest입니다. Worker 클래스는 백그라운드에서 실행할 작업의 로직을 정의하고, WorkRequest는 해당 작업을 어떻게 실행할지 지정합니다.

먼저, WorkManager 의존성을 프로젝트에 추가합니다.

build.gradle.kts (Module)kotlin
dependencies {
    implementation("androidx.work:work-runtime-ktx:2.10.0")
    // For testing
    androidTestImplementation("androidx.work:work-testing:2.10.0")
}

다음으로, 데이터를 동기화하는 간단한 Worker를 구현합니다.

kotlin
class SyncDataWorker(
    context: Context,
    params: WorkerParameters
) : Worker(context, params) {

    override fun doWork(): Result {
        return try {
            // Get input data
            val userId = inputData.getString(KEY_USER_ID)
                ?: return Result.failure()

            // Perform sync operation
            val syncResult = performDataSync(userId)

            // Return output data on success
            val outputData = workDataOf(
                KEY_SYNC_COUNT to syncResult.recordCount,
                KEY_LAST_SYNC to System.currentTimeMillis()
            )
            Result.success(outputData)

        } catch (e: NetworkException) {
            // Retry on network errors
            Result.retry()
        } catch (e: Exception) {
            // Permanent failure
            Result.failure()
        }
    }

    private fun performDataSync(userId: String): SyncResult {
        // Actual sync logic here
        return SyncResult(recordCount = 42)
    }

    companion object {
        const val KEY_USER_ID = "user_id"
        const val KEY_SYNC_COUNT = "sync_count"
        const val KEY_LAST_SYNC = "last_sync"
    }
}

이 Worker를 스케줄링하려면 OneTimeWorkRequest를 생성하여 WorkManager에 등록합니다.

kotlin
class DataSyncManager(private val context: Context) {

    fun scheduleSyncWork(userId: String) {
        val inputData = workDataOf(
            SyncDataWorker.KEY_USER_ID to userId
        )

        val syncWorkRequest = OneTimeWorkRequestBuilder<SyncDataWorker>()
            .setInputData(inputData)
            .addTag("sync_work")
            .build()

        WorkManager.getInstance(context)
            .enqueueUniqueWork(
                "user_sync_$userId",
                ExistingWorkPolicy.REPLACE,
                syncWorkRequest
            )
    }
}

enqueueUniqueWork를 사용하면 동일한 식별자를 가진 작업이 중복 스케줄링되는 것을 방지할 수 있습니다. ExistingWorkPolicy.REPLACE는 기존 작업을 취소하고 새 작업으로 대체하며, KEEP은 기존 작업을 유지합니다.

제약 조건 설정: 실행 조건 제어

WorkManager의 가장 강력한 기능 중 하나는 작업의 실행 조건을 제약으로 지정할 수 있다는 것입니다. 이를 통해 네트워크 연결이 있는 경우에만, 또는 충전 중인 경우에만 작업을 실행하는 등의 제어가 가능합니다.

kotlin
fun scheduleImageUploadWork(imageUri: String) {
    val constraints = Constraints.Builder()
        .setRequiredNetworkType(NetworkType.UNMETERED)  // WiFi only
        .setRequiresBatteryNotLow(true)                 // Battery > 20%
        .setRequiresCharging(false)                     // No charging required
        .setRequiresStorageNotLow(true)                 // Sufficient storage
        .build()

    val uploadWorkRequest = OneTimeWorkRequestBuilder<ImageUploadWorker>()
        .setConstraints(constraints)
        .setInputData(workDataOf("image_uri" to imageUri))
        .setBackoffCriteria(
            BackoffPolicy.EXPONENTIAL,
            Duration.ofMinutes(1)
        )
        .build()

    WorkManager.getInstance(context)
        .enqueue(uploadWorkRequest)
}

NetworkType에는 다음과 같은 옵션이 있습니다:

  • NOT_REQUIRED: 네트워크 불필요
  • CONNECTED: 모든 네트워크 연결
  • UNMETERED: WiFi 등 데이터 요금이 부과되지 않는 네트워크
  • METERED: 모바일 데이터 등 요금제 네트워크
  • NOT_ROAMING: 로밍 중이 아닌 네트워크

setBackoffCriteria는 작업이 Result.retry()를 반환한 경우의 재시도 간격을 설정합니다. EXPONENTIAL 정책에서는 재시도마다 대기 시간이 2배로 증가합니다(1분, 2분, 4분...).

주기적 실행 작업: PeriodicWorkRequest

백그라운드에서의 정기적인 데이터 동기화나 캐시 정리 등 반복 실행이 필요한 작업에는 PeriodicWorkRequest를 사용합니다.

kotlin
class PeriodicSyncScheduler(private val context: Context) {

    fun schedulePeriodicSync() {
        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()

        val periodicSyncRequest = PeriodicWorkRequestBuilder<SyncDataWorker>(
            repeatInterval = 6, TimeUnit.HOURS,
            flexInterval = 30, TimeUnit.MINUTES
        )
            .setConstraints(constraints)
            .setInputData(workDataOf("sync_type" to "periodic"))
            .addTag("periodic_sync")
            .build()

        WorkManager.getInstance(context)
            .enqueueUniquePeriodicWork(
                "periodic_data_sync",
                ExistingPeriodicWorkPolicy.UPDATE,
                periodicSyncRequest
            )
    }

    fun cancelPeriodicSync() {
        WorkManager.getInstance(context)
            .cancelUniqueWork("periodic_data_sync")
    }
}

flexInterval 매개변수는 작업이 실행되는 시간 범위를 지정합니다. 위 예제에서는 6시간마다 실행되며, 실행 시점은 각 간격의 마지막 30분 이내가 됩니다. 이를 통해 시스템은 여러 앱의 작업을 일괄 처리하여 배터리를 절약할 수 있습니다.

주의할 점으로, 주기적 작업의 최소 간격은 15분입니다. 이보다 짧은 간격을 지정해도 시스템은 15분으로 강제합니다.

작업 체인: 순차 실행과 병렬 실행

WorkManager에서는 여러 작업을 순차 또는 병렬로 실행하는 체인을 구성할 수 있습니다. 이미지 다운로드, 처리, 업로드와 같은 일련의 작업에 최적입니다.

kotlin
class ImageProcessingPipeline(private val context: Context) {

    fun processAndUploadImages(imageIds: List<String>) {
        val workManager = WorkManager.getInstance(context)

        // Step 1: Download images in parallel
        val downloadRequests = imageIds.map { imageId ->
            OneTimeWorkRequestBuilder<DownloadImageWorker>()
                .setInputData(workDataOf("image_id" to imageId))
                .addTag("download")
                .build()
        }

        // Step 2: Apply filters (runs after ALL downloads complete)
        val filterRequest = OneTimeWorkRequestBuilder<ApplyFilterWorker>()
            .addTag("filter")
            .build()

        // Step 3: Compress images
        val compressRequest = OneTimeWorkRequestBuilder<CompressImageWorker>()
            .addTag("compress")
            .build()

        // Step 4: Upload to server
        val uploadRequest = OneTimeWorkRequestBuilder<UploadImageWorker>()
            .setConstraints(
                Constraints.Builder()
                    .setRequiredNetworkType(NetworkType.CONNECTED)
                    .build()
            )
            .addTag("upload")
            .build()

        // Build the chain
        workManager
            .beginWith(downloadRequests)  // Parallel execution
            .then(filterRequest)          // Sequential after downloads
            .then(compressRequest)         // Sequential
            .then(uploadRequest)           // Sequential
            .enqueue()
    }
}

beginWith에 여러 WorkRequest를 전달하면 병렬로 실행됩니다. then으로 체인된 작업은 이전 모든 작업이 성공한 후에 실행됩니다.

작업 모니터링과 취소

실행 중인 작업의 상태를 모니터링하고 필요에 따라 취소하는 것은 훌륭한 사용자 경험을 위해 필수적입니다.

kotlin
class WorkStatusObserver(private val context: Context) {

    fun observeWorkStatus(workId: UUID): Flow<WorkInfo.State> {
        return WorkManager.getInstance(context)
            .getWorkInfoByIdFlow(workId)
            .map { it?.state ?: WorkInfo.State.CANCELLED }
    }

    fun observeWorkByTag(tag: String): Flow<List<WorkInfo>> {
        return WorkManager.getInstance(context)
            .getWorkInfosByTagFlow(tag)
    }

    // Usage in ViewModel
    fun monitorSyncProgress() {
        viewModelScope.launch {
            observeWorkByTag("sync_work").collect { workInfoList ->
                workInfoList.forEach { workInfo ->
                    when (workInfo.state) {
                        WorkInfo.State.RUNNING -> {
                            val progress = workInfo.progress.getInt("progress", 0)
                            _uiState.update { it.copy(syncProgress = progress) }
                        }
                        WorkInfo.State.SUCCEEDED -> {
                            val syncCount = workInfo.outputData.getInt("sync_count", 0)
                            _uiState.update { it.copy(syncComplete = true, recordCount = syncCount) }
                        }
                        WorkInfo.State.FAILED -> {
                            _uiState.update { it.copy(syncError = "Sync failed") }
                        }
                        else -> { /* Handle other states */ }
                    }
                }
            }
        }
    }
}

작업을 취소하려면 태그, 고유 이름 또는 ID를 사용합니다:

kotlin
val workManager = WorkManager.getInstance(context)

// Cancel by unique name
workManager.cancelUniqueWork("user_sync_123")

// Cancel by tag
workManager.cancelAllWorkByTag("sync_work")

// Cancel by ID
workManager.cancelWorkById(workId)

// Cancel all work (use with caution)
workManager.cancelAllWork()

CoroutineWorker를 활용한 비동기 처리

Kotlin 코루틴을 사용하는 경우 CoroutineWorker가 더 자연스러운 선택입니다. doWork가 suspend 함수가 되어 비동기 작업을 간결하게 작성할 수 있습니다.

kotlin
class CoroutineSyncWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    @Inject
    lateinit var repository: DataRepository

    override suspend fun doWork(): Result {
        return try {
            setForeground(createForegroundInfo())

            val totalRecords = 100
            var processedRecords = 0

            repository.getRecordsToSync().collect { record ->
                repository.syncRecord(record)
                processedRecords++

                // Report progress
                setProgress(workDataOf(
                    "progress" to (processedRecords * 100 / totalRecords)
                ))
            }

            Result.success(workDataOf(
                "synced_count" to processedRecords
            ))

        } catch (e: CancellationException) {
            throw e  // Don't catch cancellation
        } catch (e: Exception) {
            if (runAttemptCount < 3) {
                Result.retry()
            } else {
                Result.failure(workDataOf("error" to e.message))
            }
        }
    }

    private fun createForegroundInfo(): ForegroundInfo {
        val notification = NotificationCompat.Builder(applicationContext, "sync_channel")
            .setContentTitle("Syncing Data")
            .setSmallIcon(R.drawable.ic_sync)
            .setOngoing(true)
            .build()

        return ForegroundInfo(
            NOTIFICATION_ID,
            notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
        )
    }

    companion object {
        private const val NOTIFICATION_ID = 1001
    }
}

setForeground를 호출하면 장시간 실행되는 작업을 Foreground Service로 실행하여 시스템에 의한 취소를 방지할 수 있습니다. Android 14 이상에서는 foregroundServiceType을 명시적으로 지정해야 합니다.

Android 면접 준비가 되셨나요?

인터랙티브 시뮬레이터, flashcards, 기술 테스트로 연습하세요.

2026년 기술 면접에서 자주 출제되는 질문

Android 기술 면접에서는 WorkManager에 대한 깊은 이해가 요구되는 경우가 많습니다. 다음은 자주 출제되는 질문과 답변 포인트입니다.

Q1: WorkManager와 Foreground Service의 사용 구분은?

WorkManager는 지연 가능하지만 반드시 실행되어야 하는 작업에 사용합니다(예: 로그 업로드, 주기적 동기화). Foreground Service는 즉시 그리고 지속적인 실행이 필요한 작업에 사용합니다(예: 음악 재생, 위치 추적). WorkManager의 작업은 시스템 판단에 따라 지연될 수 있지만, Foreground Service는 즉시 시작됩니다.

Q2: Doze 모드와의 관계는?

Doze 모드 중에 WorkManager 작업은 유지보수 윈도우까지 지연됩니다. 단, setExpedited()를 사용하면 더 빠른 시점에 실행을 요청할 수 있습니다. Android 12 이상에서 setExpedited는 Foreground Service를 자동으로 사용하여 작업을 실행합니다.

Q3: Worker 내에서 의존성 주입은?

Hilt 2.50 이상에서는 @HiltWorker 어노테이션을 사용하여 쉽게 의존성을 주입할 수 있습니다:

kotlin
@HiltWorker
class HiltSyncWorker @AssistedInject constructor(
    @Assisted context: Context,
    @Assisted params: WorkerParameters,
    private val repository: DataRepository  // Injected
) : CoroutineWorker(context, params) {
    // ...
}

Q4: 테스트는 어떻게 수행하는가?

work-testing 라이브러리를 사용하여 동기적으로 Worker를 테스트할 수 있습니다:

kotlin
@Test
fun testSyncWorker() {
    val worker = TestListenableWorkerBuilder<SyncDataWorker>(context)
        .setInputData(workDataOf("user_id" to "123"))
        .build()

    val result = worker.doWork()

    assertThat(result).isEqualTo(ListenableWorker.Result.success())
}

결론

Android WorkManager는 2026년 Android 개발에서 백그라운드 작업 처리의 표준 도구로 확립되었습니다. 제약 조건의 선언적 지정, 작업 체인 구성, 그리고 CoroutineWorker를 통한 비동기 처리 지원으로 신뢰성 높은 백그라운드 처리를 간결하게 구현할 수 있습니다.

기술 면접에서는 WorkManager의 기본 개념뿐만 아니라 Doze 모드와의 관계, 적절한 사용 구분, 테스트 방법에 대해서도 설명할 수 있어야 합니다. 이 글에서 소개한 패턴을 실제 프로젝트에서 활용하여 깊은 이해를 갖추는 것이 중요합니다.

오늘의 챌린지

Android 코드의 버그를 찾을 수 있나요

실제 코드 한 조각, 숨은 버그 하나, 하루 한 번. 계정 없이 바로 도전할 수 있습니다.

Anthony Fillion-Maillet

작성자

Anthony Fillion-Maillet

SharpSkill 창업자

10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.

2026년 8월 22일 업데이트

태그

#android
#workmanager
#kotlin
#background-tasks
#jetpack

공유

관련 기사