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

2026년 현재, Android WorkManager는 백그라운드 작업 처리를 위한 표준 솔루션으로 확립되었습니다. Android 15의 배터리 최적화 강화로 인해 앱이 백그라운드에서 안정적으로 작업을 실행하는 것이 그 어느 때보다 중요해졌습니다. WorkManager는 앱이 종료되거나 기기가 재시작된 후에도 지연 가능하고 신뢰성 있는 비동기 작업을 실행하기 위한 Android Jetpack 라이브러리입니다. 이 글에서는 WorkManager의 기본 개념부터 고급 사용법, 그리고 2026년 기술 면접에서 자주 출제되는 질문까지 상세히 다룹니다.
WorkManager는 JobScheduler, AlarmManager, Firebase JobDispatcher의 기능을 통합하여 API 레벨 21 이상에서 일관된 동작을 보장합니다. 작업의 제약 조건(네트워크 연결, 충전 상태 등)을 선언적으로 지정할 수 있으며, 시스템이 작업의 최적 실행 시점을 자동으로 결정합니다. Foreground Service가 즉시 실행이 필요한 작업용인 반면, WorkManager는 지연 가능하지만 반드시 실행되어야 하는 작업에 최적화되어 있습니다.
WorkManager 기초: OneTimeWorkRequest와 Worker 클래스
WorkManager의 기본 구성 요소는 Worker 클래스와 WorkRequest입니다. Worker 클래스는 백그라운드에서 실행할 작업의 로직을 정의하고, WorkRequest는 해당 작업을 어떻게 실행할지 지정합니다.
먼저, WorkManager 의존성을 프로젝트에 추가합니다.
dependencies {
implementation("androidx.work:work-runtime-ktx:2.10.0")
// For testing
androidTestImplementation("androidx.work:work-testing:2.10.0")
}다음으로, 데이터를 동기화하는 간단한 Worker를 구현합니다.
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에 등록합니다.
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의 가장 강력한 기능 중 하나는 작업의 실행 조건을 제약으로 지정할 수 있다는 것입니다. 이를 통해 네트워크 연결이 있는 경우에만, 또는 충전 중인 경우에만 작업을 실행하는 등의 제어가 가능합니다.
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를 사용합니다.
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에서는 여러 작업을 순차 또는 병렬로 실행하는 체인을 구성할 수 있습니다. 이미지 다운로드, 처리, 업로드와 같은 일련의 작업에 최적입니다.
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으로 체인된 작업은 이전 모든 작업이 성공한 후에 실행됩니다.
작업 모니터링과 취소
실행 중인 작업의 상태를 모니터링하고 필요에 따라 취소하는 것은 훌륭한 사용자 경험을 위해 필수적입니다.
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를 사용합니다:
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 함수가 되어 비동기 작업을 간결하게 작성할 수 있습니다.
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 어노테이션을 사용하여 쉽게 의존성을 주입할 수 있습니다:
@HiltWorker
class HiltSyncWorker @AssistedInject constructor(
@Assisted context: Context,
@Assisted params: WorkerParameters,
private val repository: DataRepository // Injected
) : CoroutineWorker(context, params) {
// ...
}Q4: 테스트는 어떻게 수행하는가?
work-testing 라이브러리를 사용하여 동기적으로 Worker를 테스트할 수 있습니다:
@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-MailletSharpSkill 창업자
10년 이상 풀스택 개발을 해왔습니다. SharpSkill을 운영하며 이곳에 게시되는 모든 내용에 책임을 집니다.
2026년 8월 22일 업데이트
태그
공유
관련 기사

2026년 Android 모듈화: 멀티 모듈 아키텍처와 면접 질문
Android 앱의 멀티 모듈 설계에 대해 상세히 알아봅니다. Gradle 버전 카탈로그, 컨벤션 플러그인, 모듈 간 통신 패턴, 그리고 기술 면접에서 자주 출제되는 질문과 답변을 소개합니다.

Jetpack Navigation Compose 2026: 타입 안전 내비게이션과 면접 질문
Navigation Compose 2.10의 타입 안전 API 완벽 가이드. Kotlin Serialization 라우트 정의, 중첩 그래프, 딥링크, 면접 빈출 질문까지 상세히 다룹니다.

Kotlin Flow vs StateFlow vs SharedFlow: 2026 안드로이드 면접 질문
2026년 안드로이드 면접관이 묻는 Kotlin Flow vs StateFlow vs SharedFlow 질문을 명확한 답변, 비교 표, 실무용 코드와 함께 정리했습니다.