서비스와 작업 관리자

하이솝·2026년 5월 23일

학습 목표

  • 안드로이드에서 백그라운드 작업을 시작하는 방법을 알 수 있다.
  • 서비스(Service)를 이해하고 사용할 수 있다.
  • 작업관리자(WorkManager)를 이해하고 사용할 수 있다.
  • 작업관리자를 Foreground 서비스로 사용할 수 있다.

서비스

백그라운드로 처리할 작업을 위한 앱 컴포넌트

  • 실제 백그라운드 작업은 코루틴이나 스레드를 사용

원칙적으로 사용자 인터페이스는 제공하지 않음

서비스 종류

Started 서비스
시작 명령에 의해 시작하는 서비스로 단방향

  • Foreground 서비스
    알림(Notification)을 띄우고 사용자에게 상태를 보여주며 동작하는 서비스
  • Background 서비스
    UI를 전혀 사용하지 않는 서비스, 최근에는 거의 사용 불가

Bound 서비스
양방향으로 상호작용 가능

  • 서비스와 바운딩한 후, 서비스가 제공하는 함수들을 호출
  • 클라이언트-서버 형태로 제공하는 서비스

Started와 Bound 서버를 혼합해서 사용하기도 함

  • Started 서버로 음악을 재생시키고, Bound 서비스를 통해 제어 및 데이터 수신

서비스 만들기

<service 
	android:name=".MyService"
    android:foregroundServiceType="shortService" />

Started 서비스

Started 서비스 구현

  • Foreground나 Background 서비스 제공
  • Service 클래스의 onStartCommand() 재정의
    Foreground의 경우 startForeground() 호출

started 서비스 코드

class MyService : Service() {
	override fun onCreate() { // 서비스가 생성될 때 호출되는 콜백
    	super.onCreate()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) { 
        // Android 8.0 이상일 때
        	createNotificationChannel() // 알림 채널 생성
        }
    }
    override fun onStartCommand(intent: intent?, flags: Int, startId: Int): Int {
    	myStartForeground(notificationID, createNotification())
        
        serviceScope.launch {
        	delay(1000) // 코루틴으로 1초 대기
            for (i in 1..10) {
            	println("in service $startId#i")
                myStartForeground(notificationID, createNotification(i * 10))
                delay(1000)
            }
            stopSelf(startId)
        }
        
        return START_NOT_STICKY // 서비스가 강제 종료되면 재시작하지 않도록
    }
}
  • Android 14(Upside Down Cake) 부터
    foreground type을 지정해서 startForeground()를 호출해야 함
private fun myStartForeground(id: Int, notification: Notification) {
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES_UPSIDE_DOWN_CAKE) {
    	startForeground(
        	id,
            notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERIVE
            // foreground serive type 지정
        )
    } else {
    	startForeground(id, notification)
    }
}

Started 서비스 시작

  • 보통 액티비티에서 시작 시킴
class MainActivity : ComponentActivity() {
	... 생략 ...
    Button(onClick = {startForegroundService() }) {
    	Text("Start Service")
    }
    ... 생략 ...
    private fun startForegroundService() {
    	Intent(this, MyService::class.java).also {
        	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) {
            // Android 8.0 이상부터는 
            // 앱이 백그라운드에 있으면 서비스 실행이 제한됨
            // 포그라운드 서비스는 알림을 띄워야만 실행 가능	
            	startForegroundService(it)
                // 시스템에 포그라운드 서비스로 동작할 것이라는 예고를 보내는 함수
                // 서비스 시작 + 시스템에 포그라운드 서비스로 예고
            } else {
            	startService(it)
                // 8.0 이전 버전에는 예고가 필요 없음
            }
        }
    }
}

알림(Notification) 코드

  • 화면의 상단에 앱과 관련한 정보를 표시

  • 알림을 만들기 위해
    NotificationChannel 생성
    Notification 생성
    생성한 Notification을 알림창에 표시
    액티비티에서는 NotificationManagerCompatNotify로 알림 표시
    Foreground 서비스에서는 startForeground()로 알림 표시

  • 안드로이드 13부터 알림 권한도 필요함

<uses-permission android:name="android.permission.POST_NOTIFICATION" />
동적 권한 요청: Manifest.permission.POST_NOTIFICATIONS
private val channelID = "default"
private val notificationID = 1

// 알림 채널 생성
@RequireApi(Build.VERSION_CODES.0)
private fun createNotificationChannel() {
	val channel = NotificaitonChannel(channelID, "default channel",
    	NotificationManager.IMPORTANCE_DEFAULT)
        channel.description = "description text of this channel."
        NotificationManagerCompat.from(this).createNotificationChannel(channel)
	}
}

// 알림 생성
private fun createNotification(progress: Int = 0) 
= NotificationCompat.Builder(this, channelID)
.setContentTitle("Downloading")
.setContentText("Downloading a file from a cloud")
.setSmallIcon(R.drawable.ic_baseline_cloud_download_24)
.setOnlyAlertOnce(true) // 알람 소리가 날 때, 처음에만 나게 함
.setProgress(100, progress, false)
.build()

// 알림 표시 또는 업데이트
private fun showNotification(id: Int, notification: Notification) {
	if (AvtivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
    == PackageManager.PERMISSION_GRANTED) {
    	NotificationManagerCompat.from(this).notify(id, notification)
    }
}

Bound 서비스

  • 클라이언트-서버 방식에서 서버에 해당

  • 클라이언트는 액티비티 등이 되고, 다른 앱의 컴포넌트도 될 수 있음
    다른 앱의 컴포넌트가 클라이언트가 된다면
    IPC(Inter Process Communication)가 됨

Bound 서비스 종류

Binder를 상속하여 만들기

  • 한 프로세스에서만 사용할 경우

Messager나 AIDL 사용

  • 여러 프로세스가 사용할 경우
  • IPC가 가능한 방법
  • AIDL보다 Messenger 사용 하고 쉽고, 권장

Bound 서비스 코드

class MyService : Service() {
	private val binder = LocalBinder()
    
    inner class LocalBinder : Binder() {
    	fun getService() = this@MyService
    }
    
    override fun onBind(intent: Intent): IBinder {
    	return binder
    }
    
    var startedCount = 0
    	private set
        
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    println("MyService:onStartCommand $startId")
    startedCount++
    
    return super.onStartCommand(intent, flags, startId)
}

Bound 서비스 Bind 하고 사용

class MainActivity : ComponentActivity() {    
	override fun onCreate(savedInstanceState: Bundle?) {
		...
		setContent {
			BackgroundTasksTheme {
				Surface( ... ) {
					if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
						RequestPermission()
						MainScreen( onGetServiceCount = { myService?.startedCount ?: 0 },  ... )
					}
				}
			}
		}
        private var myService: MyService? = null
        
        private val serviceConnection = object : ServiceConnection {
        	override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            	myService = (service as MyService.LocalBinder).getService() 
                // 바운드됨
            }
            override fun onServiceDisconnected(name: ComponentName?) {
            	myService = null
            }
        }
        
        override fun onStart() {
        	super.onStart()
            Intent(this, MyService::class.java).also {
            	bindService(it, serviceConnection, BIND_AUTO_CREATE)
                // Bound 서비스 bind, 결과가 serviceConnection으로 전달
            }
        }
        
        override fun onStop() {
        	super.onStop()
            unbindService(serviceConnection)
        }
    }

권한

  • 제한된 정보나 행위를 접근/수행하기 위해 권한 필요
  • 권한이 없는 앱이 특정 정보에 접근하려고 하면 예외 발생
  • 해당 정보 접근에 필요한 권한을 앱에 부여해야 함

설치 시 권한 부여

  • Manifest 파일에 권한을 표시만 하면 설치할 때 부여됨

동적 권한 부여

  • 보통 민감한 정보와 관련된 권한, 실행 시 사용자에게 요청
    API 문서에 dangerous라고 표시되어 있음

권한 예시

Manifest.permission.FOREGROUND_SERVICE_X: Foreground 서비스
Manifest.permission.POST_NOTIFICATIONS: 알림 표시 권한

동적 권한 부여 방법

AndroidManifest.xml에 권한을 표시하고 설치시에 사용자에게 확인

앱이 실행 중에 권한이 필요할 때 사용자에게 권한 부여 요청

  • 설치할 때가 아니고 사용할 때 권한을 요청하여 부여
  • 일시적으로 권한을 부여하거나, 앱이 사용 중일 때만 권한을 부여할 수 있음
    보통 위치 접근과 같이 민감한 정보에 대한 권한일 경우

동적 권한 요청하기 전에 사용자에게 권한의 필요성을 설명하도록 권장

  • 앱 설치 후 실행하면 필요한 권한에 대해 설명을 보여주는 화면 보여줌

AndroidManifest.xml에 권한 표시

<uses-permission android:name=
                 "android.permission.FOREGROUND_SERVICE_DATA_STNC" />
<uses-permission android:name=
                 "android.permission.POST_NOTIFICATIONS" />

동적 권한 예제 코드

@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@Composable
fun RequestPermission() {
	val context = LocalContext.current
    val permission = Manifest.permission.POST_NOTIFICATIONS
    
    var showRationale by remember { mutableStateOf(false) }
	var showWarning bt remember { mutableStateOf(false) }
    
    val requestPermLauncher = rememberLauncherForActivityResult
    (ActivityResultContracts.RequestPermission()) {
    	isGranted -> if (!isGranted) showWarning = true
    }
    
    LaunchedEffect(Unit) {
    	permission.let {
        	if (context.checkSelfPermission(it) != 
            PackageManager.PERMISSION_GRANTED) {
            	if ((context as? ComponentActivity)?.shouldShowRequestPermissionRationale(it) == true) {
                	showRationale = true
                } else {
                	requestPermLauncher.launch(it)
                }
            }
        }
    }
    if (showRationale) {
    	AlertDialog(
        	onDismissRequest = { showRationale = false },
            title = { Text("Notification Permission") },
            text = { Text(stringResource
            (R.string.req_permission_reason, "Notifications")) },
            confirmButton = { 
            	TextButton(onClick = { 
                	showRationale = false
                    requestPermLauncher.launch(permission)
                }) { Text("Allow") }
            },
            dismissButton = { 
            	TextButton(onClick = { showRationale = false }) { Text("Deny") }
            }
        )
    }
    
    if (showWarning) { 
    	AlertDialog(
        	onDismissRequest = { showWarning = false },
            title = { Text("Permission Denied") },
            text = { Text(stringResource
            (R.string.no_permission, "Notifications")) },
            confirmButton = {
            	TextButton(onClick = {showWarning = false}) { Text("OK") }
            }
        )
    }
}

WorkManager

  • 스케줄 가능한 비동기 작업을 쉽게 만들 수 있는 API
  • 앱이 종료되거나 디바이스가 재부팅되어도 동작
  • 이전의 다양한 백그라운드 스케줄 API를 하나로 통일

Foreground Service vs Work Manager

  • Foreground Service
    오래 백그라운드로 작업을 해야 하는 경우

  • Work Manager
    주기적인 작업이나 기타 다른 백그라운드 작업 대부분

Work Manager 용도

  • 앱이 종료되거나 디바이스가 재부팅되더라도
    실행이 보장되는 백그라운드 작업

  • 백엔드 서비스로 로그 등을 보내기, 주기적으로 서버와 데이터 동기화

  • 특정 조건(네트워크나 저장 공간)이 만족될 때만 실행 가능하도록 할 수 있음

Work Manager 필요한 클래스

WorkManager

  • Worker를 시작하거나 멈추거나 현재 상태를 보기 위한 관리자
  • WorkManager.getInstance(컨텍스트)

Worker

  • 작업을 정의한 클래스
  • doWork()를 재정의하여 할 일을 정의함

WorkRequest

  • Worker와 제약 조건 등을 기술한 요청 객체
  • WorkManager에게 WorkRequest 객체를 전달하여 작업 시작
  • 작업의 종류는 크게 한번 수행 작업반복 수행 작업으로 나눌 수 있음
    OneTimeWorkRequest, PeriodWorkRequest

MyWorker.kt:Worker 정의

class MyWorker(appContext: Context, params: WorkerParameters) : 
CoroutineWorker(appContext, params) {
	override suspend fun doWork(): Result {
    	setForeground(createForegroundInfo("Starting Download"))
        // worker를 foreground 서비스로 실행하도록 설정
        // 일반적인 foreground 작업에서는 호출하지 않아도 되지만,
        // 오래 걸리는 작업에서는 foregroundService처럼 사용하기 위해 호출해야 함
        // startForeground(notificationId, notification)와 역할이 동일함
        for (i in 1..10) {
        	delay(1000)
            setForeground(createForegroundInfo("Downloading ${i * 10}%"))
            // 알림 업데이트
        }
        return.Result.success()
    }
    
    companion object {
    	const val name = "com.example.backgroundtasks.MyWorker"
    }
}

Worker를 Foreground 서비스에서 수행

Manifest에 FORGROUND_SERVICE_DATA_SYNC 권한 추가

  • 포그라운드 서비스에서 데이터 동기화 작업을 할 수 있도록
    Android 시스템에 권한을 요청
<uses-permission android:name=
                 "android.permission.FORGROUND_SERVICE_DATA_SYNC" />

Manifest에 <service> 추가

  • WorkManager가 내부적으로 사용하는 서비스 등록
    foregroundServiceType="dataSync"는 해당 서비스가
    데이터 동기화 타입의 서비스임을 시스템에 명시
<service android:name=
         "androidx.work.impl.foreground.SystemForegroundService"
         android:exported="false"
         android:foregroundServiceType="dataSync" />

Worker를 Foreground 서비스에서 수행

  • doWork() 에서 setForeground(foregrondInfo) 호출
  • foregroundInfo는 알림을 만드는 것, 아래와 같이 만들 수 있음
private fun createForegroundInfo(progress: String): ForegroundInf {
	val title = "Playing Music"
    val cancel = "Cancel Task"
    
    val intent = 
    WorkManager.getInstance(applicationContext).createCancelPendingIntent(id)
	if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.0) {
    	createNotificationChannel()
    }
    
    val notification = 
    NotificationCompat.Builder(applicationContext, channelID)
		.setContentTitle(title).setContentText(progress)
    	.setSmallIcon(R.drawable.baseline_music_note_24)
    	.setOngoing(true).setOnlyAlertOnce(true)
    	.addAction(android.R.drawable.ic_delete, cancel, intent)
    	.build()
    return if (Build.VERSION_SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
    	ForegroundInfo(notificationID, notification,
        FOREGROUND_SERVICE_TYPE_DATA_SYNC)
    } else {
    	ForegroundInfo(notificationID, notification)
    }
}

Worker 시작과 종료

  • 일회성 작업 시작하기
val oneTimeRequest = OneTimeRequestBuilder<MyWorker>() // 일회성 작업
	.setConstraints(constraints) // 제약 조건 부여
	.build() // 빌드, workrequest 객체 생성/리턴
    
WorkManager.getInstance(this).enqueueUniqueWork(
	MyWorker.name, // MyWorker에서 정의한 식별자용 이름
    ExistingWorkPolicy.KEEP, // 동일한 Worker가 있을 때 처리 방법
    // KEEP은 기존 것을 유지
    oneTimeRequest)
)
  • 반복 작업 시작하기 (15분 주기)
val repeatingRequest = 
PerioidWorkRequestBuilder<MyWorker>(15, TimeUnit.MINUTES) // 15분 주기
	.setConstraints(constraints) // 제약 조건 부여
    .build() // 빌드, workrequest 객체 생성/리턴
    
WorkManager.getInstance(this).enqueueUniqueWork(
	MyWorker.name, // MyWorker에서 정의한 식별자용 이름
    ExistingWorkPolicy.KEEP, // 동일한 Worker가 있을 때 처리 방법
    // KEEP은 기존 것을 유지
    oneTimeRequest)

WorkRequest 제약 조건

  • Worker에 제약 조건 부여, 조건을 만족할 때만 실행
    특정 네트워크 종류, 비과금 네트워크에서만
    배터리 상태 조건, 배터리가 충분할때만
    충전 중에만
    디바이스가 유휴상태일 때만
val constraints = Constraints.Builder().apply {
	setRequiredNetworkType(NetworkType.UNMENTERED) // Wi-Fi
	setRequiresBatteryNotLow(true) // 배터리가 적을 때
    // setRequiresCharging(true) // 충전 중 일 때
    // setRequiresDeviceIdle(true) // 유휴상태일 때
}.build()

Worker 취소

  • Worker의 식별자용 이름으로 cancelUniqueWork()
WorkManager.getInstance(this).cancelUniqueWork(MyWorker.name)

Worker 상태 확인

Worker 식별자 이름으로 현재 Work Info를 가져옴(Flow를 통해)

  • Flow를 State로 변환, collectAsState

Worker 상태

  • ENQUEUED: worker가 큐에 들어가서 대기 중, 시간이 되면 시작함
  • RUNNING: worker 실행 중
  • SUCCEEDED: worker 실행 완료/성공 OneTimeWorker만 해당됨
  • CANCELED: worker 취소됨
val workInfos by WorkManager.getInstance(context).
getWorkInfosForUniqueWorkFlow(MyWorker.name).
collectAsStateWithLifecycle(initialValue = emptyList())

Background Task Inspector

  • 백그라운드 작업을 모니터링하는 도구

Worker 시작은 어디서 하는게 좋을까

액티비티 시작
Application 시작에서 worker 시작

  • MyApplication: Application()으로 만들고 onCreate() 재정의하여
    onCreate()에서 Worker시작*

  • MyApplicationAndroidManifest에 등록해야 함

  • Worker는 백그라운드로 실행하므로 UI 관련 코드는 없어야 함

Application 만들기

  • Application 클래스와 Manifest에 등록
import android.app.Application

class MyApplication: Application() {
	override fun onCreate() {
    	super.onCreate()
        
        startWorker()
    }
    
    private fun startWorker() {
    // ... 생략 ...
    }
}
<application android:name=".MyApplication">
</application>

0개의 댓글