본문 바로가기

개발 코딩 정보 공유/개발후기

백그라운드 개발 후기(WorkManager, 포그라운드서비스)

 

안녕하세요. 블루스웨터소프트 입니다.

프로젝트를 진행하며 겪은 이야기와 기술적인 이야기를 작성하고 있습니다.

클라이언트단에서 사용할 특정 sdk를 개발하는 중에 급하게 연락이 왔습니다.

"기존의 기능들을 백그라운드에서 동작하게 할수 있을까요?"

"네. 되긴 됩니다만..."

AOS, IOS 백그라운드 제한에 대해 말이 길어지게 되었고 관련 내용을 정리하여 보내주기로 합니다😅

Google, Apple은 업데이트를 거듭할수록 백그라운드 동작에 대해 매우 엄격한 제제를 가하고 있습니다.

그도 그럴것이 유저의 보안, 기기의 퍼포먼스적인 측면에서 백그라운드 동작은 매우 불리하기 때문입니다.

특정앱이 나도 모르게 무한정 실행되고 있다고 생각해보시면 어떻게 될까요? 배터리는 배터리대로, 보이지도 않기에 무슨 앱이 어떻게 동작하고 있는지 뭘 수집하는건지 알기도 어렵습니다. 여타 여러가지 이유들로 인해 대세는 백그라운드 제한인 것 입니다. 🥲

 

초기에는 백그라운드 동작에 대해서 굉장히 관대했습니다. 특히나 안드로이드에서는 service 에 모든 처리를 위임한 백그라운드 앱들이 난무하였습니다. 안드로이드 8.0 OREO 버전을 시작으로 굉장히 엄격해진것으로 기억합니다. 런타임 퍼미션 같은 새로운 권한 관리가 생겼고, 백그라운드 실행을 위해 Foreground service 등이 필수로 등장했습니다. 

오래전에 해당 관련 글을 자세하게 썼던 기억이있습니다.

 

포그라운드 서비스의 구현

간단하게 기존의 foreground service 구현을 확인해보겠습니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>
    <service
        android:name=".MyMediaPlaybackService"
        android:foregroundServiceType="mediaPlayback"
        android:exported="false">
    </service>
</manifest>

 

메니페스트에 service 타입을 명시해야합니다. (target API에 따라서)

더보기

참고: 앱에서 타겟팅하는 API 수준에 따라 앱 매니페스트에서 포그라운드 서비스를 반드시 선언해야 할 수 있습니다.

  • API 수준 29 이상: location 서비스 유형을 사용하여 위치 정보를 사용하는 모든 포그라운드 서비스를 선언해야 합니다.
  • API 수준 30 이상: camera 또는 microphone 서비스 유형을 사용하여 카메라나 마이크를 사용하는 모든 포그라운드 서비스를 각각 선언해야 합니다.
  • API 수준 34 이상: 모든 포그라운드 서비스는 서비스 유형과 함께 선언해야 합니다.

포그라운드 서비스를 만들려고 하는데 매니페스트에서 해당 유형이 선언되지 않았다면 startForeground() 호출 시 시스템에서 MissingForegroundServiceTypeException이 발생합니다.

필요하지 않더라도 모든 포그라운드 서비스를 선언하고 서비스의 서비스 유형을 제공하는 것이 좋습니다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android" ...>

    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA"/>

    <application ...>
        ...
    </application>
</manifest>

메니페스트에 권한을 요청해야 합니다. API28 이상을 타겟팅 한다면 반드시 권한을 작성해야합니다.

class MyCameraService: Service() {
//...

  	private fun startForeground() {
        val cameraPermission =
                ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        if (cameraPermission == PackageManager.PERMISSION_DENIED) {
            //권한 거부 처리
            return
        }

        try {
            val notification = NotificationCompat.Builder(this, "CHANNEL_ID") //채널ID
                .build()
            ServiceCompat.startForeground(
                this,
                100,
                notification,
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
                    ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA //특정 타입 명세
                } else {
                    0
                },
            )
        } catch (e: Exception) {
            // ...
        }
  	}
}

그리고 서비스에서 channel, notification 등을 생성후 startForegroundService를 실행하게 됩니다. 문서를 보면 아시겠지만 최근에 디테일한 부분이 굉장히 많이 바뀌었고 복잡해 졌습니다. 문서를 잘 보고 다시 작업 해야 합니다.🤪

 

어찌되었든 ForegroundService를 써야 하는 이유는 명확했습니다. 백그라운드에서 동작시 최대한 동작이 보장되어야 하기 때문 입니다. (메모리 상황에 따라서 죽기도 함) 때문에 포그라운드 실행시 noti, channel 등을 생성하여 알림창을 띄우고 유저에게 동작을 알리게 됩니다. 공식 문서의 내용을 보다보니 포그라운드 서비스 실행 제한 이라는 문서를 보게 되었습니다. 설마...🤦

더보기

Android 12를 타겟팅하는 앱은 몇 가지 특수한 사례를 제외하고 백그라운드에서 실행되는 동안 더 이상 포그라운드 서비스를 시작할 수 없습니다. 앱이 백그라운드에서 실행되는 동안 포그라운드 서비스를 시작하려고 하지만 포그라운드 서비스가 예외 사례 중 하나를 충족하지 못하면 시스템에서 ForegroundServiceStartNotAllowedException이 발생합니다.

구글에서 매년 타겟 버전업을 강제로 시키고 있는 상황에서 이제 다른 방법으로 백그라운드 작업을 실행해라. 라는 말이었습니다.

 

WorkManager의 사용

개발하고있는 앱의 경우 문서에서 이야기 하는 몇가지 특수한 사례에 포함될것 같지 않았습니다. 🥲 때문에 WorkManager 를 사용하여 안전하게? 백그라운드 처리를 하기로 합니다. 사실 그동안에 WorkManager는 주기적인 예약 작업등에 많이 사용하도록 권장되어왔습니다.

심지어 앱이 꺼져도 백그라운드에서 작업을 처리 해야할때 유용합니다. (배치같이) WorkManager가 처리하는 지속적인 작업의 유형은 세 가지입니다.

  • 즉시: 즉시 시작하고 곧 완료해야 하는 작업입니다. 신속하게 처리될 수 있습니다.  (약 3분이상)
  • 장기 실행: 더 오래(10분 이상이 될 수 있음) 실행될 수 있는 작업입니다. (약 10분이상)
  • 지연 가능: 나중에 시작하며 주기적으로 실행될 수 있는 예약된 작업입니다.

우리 앱이 해당되는 부분은 장기 실행 부분 입니다. 대량의 파일 읽기 쓰기나 DB처리, 네트워크처리 등 다양한 이유로 백그라운드 작업이 필요할수 있습니다. 

 

커스텀 worker 생성

Custom Worker - doWork 을 통해 장기 실행이 필요한 작업을 이곳에서 작성하게 됩니다.

class CustomWorker(var ctx: Context, var params: WorkerParameters) : CoroutineWorker(ctx, params) {
    private val TAG = "CustomWorker"

    override suspend fun doWork(): Result {
        val outputData = workDataOf("outputData" to "success")

        return try {
            //장기화 실행 포그라운드 처리
            setForeground(createForegroundInfo(ctx, "starting..."))

            initNotification(ctx)

            Log.d(TAG, "=== 이미지 분석 시작 : ${Date()} ===")

            loadImagesAndDocument(ctx)
            Result.success(outputData)

        } catch (e: Exception) {
            Log.e(TAG, "=== err CustomWorker ===")
            Result.failure()

        }
    }
}

 

포그라운드 처리를 위해 CoroutineWorker, setForeground 를 설정한뒤 notification, channel 등도 지정합니다. 기존의 foreground service 와 동일 합니다. 

private fun createForegroundInfo(ctx: Context, progress: String): ForegroundInfo {
        val id = CHANNEL_ID
        val title = "장기화작업"
        val cancel = "취소"

        val intent = WorkManager.getInstance(applicationContext)
            .createCancelPendingIntent(getId())

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createChannel(ctx)
        }

        val notification = NotificationCompat.Builder(applicationContext, id)
            .setContentTitle(title)
            .setTicker(title)
            .setContentText(progress)
            .setSmallIcon(R.drawable.custom_icon)
            .setOngoing(true)
//            .addAction(R.drawable.ic_delete, cancel, intent)
            .build()

        return ForegroundInfo(NOTIFICATION_ID, notification)
    }


    fun createChannel(context: Context){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name = CHANNEL_NAME
            val description = CHANNEL_DESCRIPTION
            val importance = NotificationManager.IMPORTANCE_HIGH
            val channel = NotificationChannel(CHANNEL_ID, name, importance)
            channel.description = description

            val notificationManager =
                context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager?
            
            notificationManager?.createNotificationChannel(channel)
        }

    }

 

Worker를 만들었다면 실행은 굉장히 간단합니다. uniqueWork를 통해 정책과 초기화 단계에서 작업할 워커를 생성하고 각종 제약조건을 걸어줍니다. 충전중일때 동작할것인지, 네트워크는 연결되어야 할지 등등 많은 옵션을 지정할수 있습니다. 마지막으로 Custom Worker를 셋팅하여 최종 enqueue 를 통해 실행을 시킵니다.

 /**
 * 백그라운드 작업 - Workmanager
 */
    private fun executeWithWorkManager(context: Context){

        val workManager = WorkManager.getInstance(context)

        var continuation = workManager
            .beginUniqueWork(
                WORK_NAME,
                ExistingWorkPolicy.REPLACE,
                OneTimeWorkRequest.from(initWorker::class.java) //초기화 작업을 위한 워커
            )

        val constraints = Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresCharging(false)
            .build()

        val doBuilder = OneTimeWorkRequestBuilder<CustomWorker>()
            .setConstraints(constraints)
            .addTag(TAG_CUSTOM_WORK)
            .build()

        continuation = continuation.then(doBuilder)
        continuation.enqueue()

    }

 

이와 같이 Work Manager를 통해 장기화 실행 처리를 진행했습니다. 이 장기화 라는 말에 속아 무한정 돌것이라는 기대는 안하는게 좋습니다. 동작시간은 최대시간 600초(10분) 정도가 한계입니다. 😭 (IOS는 더 제한적입니다 약 3분...)

 

마무리글

기기에 따라서 OS에 따라서 조금씩 다르지만 공통적인것은 구글이나 애플에서 정책적으로 백그라운드 제한을 심화 하고 있다는 것입니다. 그 덕에 개발자들은 변경된 정책을 보고 또 보고 업데이트 해야 합니다. 이러한 내용들을 사전 리서치하고 전달하는것 그리고 기획자를 잘 설득시키는것 또한 일이라고 하겠습니다. 안되는걸 되게 할수는 없지 않겠습니까?🤩

역시나 쉬운게 없다고 생각하며... 🥹 

 

 

 

참고 문서

https://developer.android.com/about/versions/12/foreground-services?hl=ko

https://developer.android.com/topic/libraries/architecture/workmanager/advanced/long-running?hl=ko

https://developer.android.com/develop/background-work/services/foreground-services?hl=ko