안드로이드 백그라운드에 대해서

woga·2022년 4월 30일
3

Android 공부

목록 보기
25/49
post-thumbnail

우리는 앱을 이용하면서 백그라운드 서비스를 이용하는 일이 많다.
음악을 계속 듣는다던지, 영상이 어떤 화면에서든 스트리밍이 가능하다던지 (ex. 갤럭시는 pip로) 백그라운드는 정말 다양하게 쓰인다.

하지만 성능 역시 빠질 수가 없는데 구글에서는 이를 위해 백그라운드 처리 관련해서 매번 달라지곤 했다.

공식 문서 속 백그라운드 처리 가이드

백그라운드 작업은 다음 기본 카테고리 중 하나에 속합니다.

- Immediate: Needs to execute right away and complete soon.
- Long Running: May take some time to complete.
- Deferrable: Does not need to run right away.

위 카테고리 추천 방법은 각자 다르다.

Immediate work(즉시 실행해야 하는 작업)

Immediate work는 바로 즉시 실행해야 하는 업무를 포함한다. Immediate work는 사용자에게 중요하거나 나중에 지연된 실행을 예약할 수 없는 작업이다. 사용자가 애플리케이션을 백그라운드로 전환하거나 기기를 다시 시작하더라도 신속하게 실행되도록 예약된 상태를 유지해야 할 정도로 매우 중요하다.(They are important enough that they might need to remain scheduled for prompt execution even if the app closes or the device restarts.)

Persistent immediate work

WorkMager를 OneTimeWorkRequest를 이용해서 쓴다. setExpedited()를 사용해서 WorkRequest를 신속하게 처리한다.

Impersistent immediate work

사용자가 특정 범위를 벗어나거나 상호작용을 완료할 때 종료해야 하는 작업에는 Kotlin 코루틴을 사용하는 것이 좋다. Java 프로그래밍을 사용하는 경우, RxJava / Guava / Executors 를 사용해야 한다.

Examples

1)
앱은 데이터 소스로 부터 데이터를 갖고와야 하는데, 이 때 메인스레드에서 네트워크 요청을 하면 blocked되어 UI 처리 성능이 저하된다. 그래서 대신 코루틴의 메인 스레드에서 request를 한다.

2)
채팅 앱에서 메세지를 보내는 경우에 앱은 Worker를 WorkRequest task를 queue에 넣는다. It expedites the WorkRequest with setExpedited().

미디어 재생 또는 활성 탐색과 같은 특정한 경우에는 포그라운드 서비스를 직접 사용하는 것이 좋다.

Long-running work (장기 작업)

작업을 완료하는데 10분 이상 걸릴 거 같으면 장기작업이라고 한다.

WorkManager를 사용하면 장시간 실행되는 Worker를 사용하여 이런 장기 작업들을 처리할 수 있다.

가능한 경우, 워크로드를 나누고 작업을 지연 가능한 작업으로 처리해야 합니다. 작업량을 나눌 수 없는 경우에만 장시간 실행되는 작업자를 사용하는게 좋다. (You should chunk workloads and handle tasks as deferrable work. Only use a long-running Worker where you can't chunk your workload.)

Example

앱은 나눌 수 없는 큰 파일 하나를 다운로드 해야 한다고 할 때, 장시간 실행되는 Worker를 생성하고 다운로드 큐에 넣는다. 그러면 앱은 15분 이상 백그라운드에서 파일을 다운로드 할 것이다.

Deferrable work (지연 작업)

지연 가능한 작업은 바로 실행할 필요가 없는 작업이다.

사용자 상호작용에 직접 연결되지 않고 향후 언제든지 실행할 수 있는 모든 작업은 지연될 수 있다. 지연된 작업에 추천하는 해결 방법은 WorkManager이다.

WorkManager를 사용하여 지연된 작업을 scheduling하자. WorkManager를 사용하면 앱이 종료되거나 기기가 다시 시작되더라도 지연될 수 있는 비동기 작업을 쉽게 예약할 수 있다.
(이러한 유형의 작업을 예약하는 방법은 WorkManager 문서를 참고!)

Example

앱에서 백엔드의 데이터를 정기적으로 동기화하려고 하는 경우를 생각해보자. 사용자가 동기화를 하겠다고 트리거하지 않으며, 장치가 idle(유휴 상태, 프로세스가 실행하고 있지 않은 상태)일 때 작업이 수행되어야 된다. 이 때 권장되는 접근 방식은 Custom Worker를 이용한 PeriodicWorkRequest와 Constraints를 사용하는 것이다.

Alarm(정시에 실행해야 하는 작업)

알람은 백그라운드 작업의 일부가 아닌 특수 사용 사례이다. 위에서 설명한 두 가지 솔루션인 코루틴과 Work Manager를 통해 백그라운드 작업을 수행해야 하는데, 정확한 시점에 실행해야 하는 작업에는 AlarmManager를 사용할 수 있다.

주의할 점은, AlarmManager를 사용하여 백그라운드 작업을 예약하면 장치를 Doze 모드에서 해제하므로 배터리 수명과 전체 시스템 상태에 부정적인 영향을 미칠 수 있다는 걸 기억하자.

Example

알람 시계 또는 캘린터 이벤트와 같이 정확한 알람을 예약하는 경우에만 AlarmManager를 사용해야 한다.

Replacing foreground services

안드로이드 12는 포그라운드 서비스 시작을 백그라운드에서 제한한다. 대부분의 경우 포그라운드 서비스를 직접 처리하는 대신 WorkManager에서 setForground()를 사용해야 한다. 이를 통해 WorkManager는 Foreground 서비스의 라이프사이클을 관리하여 효율성을 보장할 수 있기 때문이다.

그래도 오래 실행되며 사용자에게 계속 진행 중임을 알려야 하는 작업을 수행하려면(ex. 음악앱) 포그라운드 서비스를 사용해야 합니다.

포그라운드 서비스를 직접 사용하는 경우 리소스 효율성을 유지하기 위해 서비스를 올바르게 종료해야 합니다.

Some use cases for using foreground services directly are as follows

  • Media playback (미디어 재생)
  • Activity tracking
  • Location sharing (위치 공유)
  • Voice or video calls (음성 혹은 비디오 통화)

그냥 Thread 따로 쓰면 되는거 아니야? 왜 서비스로 따로 사용할까?

Thread의 문제점: 안드로이드 컴포넌트가 아니므로 독자적인 생명주기도 없을 뿐더러 Main Thread가 아니기 때문에 앱을 나가면 프로세스가 유지되지 않는다. 또, 만약에 OOM Killer에 의해서 프로세스가 종료되면 다시 재시작 될 것이라는 보장도 없다.

하지만 Service는: 안드로이드 4대 컴포넌트 중 하나로서 독자적인 생명주기를 가지고 있고 Main Thread에서 동작하기 때문에 사용자가 앱을 나가도 프로세스가 유지된다. 만약 강제로 프로세스가 죽을 경우 다시 살아날 수도 있다.

백그라운드 처리는 이제껏 어떻게?

사실 구글 지금까지 수많은 처리 가이드는 추천해왔다. 현재 처리 가이드 속 WorkManager가 나오기까지 백그라운드는 담당하는 라이브러리에 대한 시행착오가 있었다.

백그라운드의 문제?

Background Service는 사용자가 인지할 필요가 없는 작업을 수행함으로 상호작용 하지 않는다.

이는 장점 같지만, 앱 입장에서는 성능 저하를 일으킬 만한 특성이다.
무분별하게 Background Service가 사용된다면 사용자는 이를 인지하지도 못할 것이고, 이로 인해 디바이스가 과부화 되어 메모리 부족을 겪을 수 있고 심하면 앱이 갑자기 죽는 일들이 일어날 수 있기 때문이다.

1 : Background 제한

Google은 이 점을 인지하고 Oreo 버전부터 Background Service를 제한시켜버린다. 정확히는 앱이 Closed 상태일 때의 Background Service를 제한한 것이다. 앞으로는 앱이 Background 상태일 때도 Service를 유지 시키려면 Foreground Service만 사용 가능해진 것이다. 즉, 사용자가 Service를 계속 돌고 있음을 인지하고 직접 관리할 수 있도록 한 것이다.

더 나아가 Google은 startForegroundService()라는 메서드를 만들었다. 이 메서드는 Service 시작 후 Service 내에서 5초 안에 startForeground()를 호출하지 않으면 ANR을 띄우는 메서드다. Closed상태의 앱에서 서비스는 startForegroundService() 메서드를 사용하게 됐다.

그러면 다 Foreground Service로 바꿔서 관리해야하나? 그럼에도 똑같이 과부하가 올 것인데?

2 : JobScheduler

그래서 Google은 예약된 작업을 해결법으로 제시했고 이를 위해 JobScheduler를 추천했다.

JobScheduler란? 개발자가 Background task를 정의하고 언제 이 작업이 실행될지 타이밍을 정할 수 있게 도와준다.

하지만 JobScheduler에서도 문제가 있었다. 원인은 버전 때문이었는데, JobScheduler는 롤리팝(버전 21)부터 지원하지만 롤리팝 버전에서 JobScheduler는 불안정하다는 이슈가 있었고 제대로 사용하려면 마쉬멜로우(버전 23)부터 사용해야 했습니다. 그 이전의 버전에서는 AlarmManager와 Broadcast Receiver를 사용해야 했다.

Q) 그럼 그냥 AlarmManager랑 Broadcast Receiver 사용하면 되잖아?

하지만 AlarmManager도 마쉬멜로우부터 문제가 생겼다. Doze 모드가 생기면서 알림이 울리지 않는 경우가 생겼고 Google은 setAndAllowWhileIdle(), setExactAndAllowWhileIdle() 같은 메서드를 지원하여 해결할 수 있다고 했지만 이외에도 AlarmManager를 잘못 설계하면 배터리 소모가 심해질 수 있다는 문제점이 발견됐다. 또한, 오레오(버전 26)부터 Background Service와 함께 암시적 Broadcast가 제약을 먹으면서 사용하기 더 어려워졌다.

2-1 : JobDispatcher

이런 상황을 인지한 Firebase가 JobDispatcher를 제공한다. JobDispatcher는 버전이 마쉬멜로우 버전 이상이라면 JobScheduler를, 미만이라면 AlarmManager를 사용하도록 해줌으로서 버전을 나누어 코드를 짜는 개발자들은 조금 편해지는 듯 했으나..

if (Build.VERSION.SDK_INT >= 23) {
    // use JobScheduler
} else {
    // use AlarmManager
}

Firebase와 관련된 것을 사용하려면 Google Play Service에 의존성을 가지게 되고 이로 인해 글로벌 앱들은 문제가 생긴다.

해서.. 서드 파티 라이브러리를 이용하기도 했다는데..

(중국이 바로 이 서비스를 사용하지 못하게 막혀있다고 하네요)

3 : WorkManager

2018 구글 I/O에서 WorkManager를 발표하고 만다. 완전히 새로운 방법으로 처리하는 것이 아니라 이전의 JobDispatcher 처럼 OS 버전별로 필요한 처리를 핸들링할 수 있게 도와준다.

위의 디스패처처럼 Google Play Service에 대한 의존성도 없고 확장된 여러 기능을 제공하기도 한다.

WorkManager 장점

  • Android가 제시하는 best practice에 가까워 전력소비를 줄일 수 있다.

  • 일회성이나 장기적인 작업을 지원한다.

  • Chaining을 지원하여 보다 정확하고 명확한 순서를 볼 수 있다.

  • 예약된 작업은 내부적으로 관리되는 SQLite 데이터베이스에 저장되어서 기기를 재부팅해도 작업이 유지되고 다시 예약되도록 보장한다.

  • RxJava와 Coroutine을 지원한다.

WorkManager 사용

  • 워커를 만들어주고 (위에서 숱하게 말이 나왔던 Worker)
class LogWorker(appContext: Context, workerParams: WorkerParameters):
    Worker(appContext, workerParams) {
    override fun doWork(): Result {
        (0..100).forEach {
            logging(it)
        }
        return Result.success()
    }

    private fun logging(count: Int) {
        Log.e("log", count.toString())
        Thread.sleep(1000)
    }
}
  • Worker를 WorkRequestBuilder로 WorkRequest 형태로 빌두 후 WorkManager에 enqueue()

class LogActivity : AppCompatActivity() {
    private lateinit var binding: ActivityLogBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityLogBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.button.setOnClickListener {
            OneTimeWorkRequestBuilder<LogWorker>()
                    .build().also {
                        WorkManager.getInstance(this)
                                .beginWith(it)
                                .enqueue()
                    }
        }

    }
}
  • 그 외 기능들
// Constraints + PeriodicWorkRequest + InputData

val constraints = Constraints.Builder()
   .setRequiredNetworkType(NetworkType.UNMETERED)
   .setRequiresCharging(true)
   .build()

val myWorkRequest: WorkRequest =
   PeriodicWorkRequestBuilder<MyWork>(
           1, TimeUnit.HOURS, // repeatInterval (the period cycle)
           15, TimeUnit.MINUTES // flexInterval
       )
       .setInputData(
           workDataOf(
               "IMAGE_URI" to "http://..."
            )
        )
       .setConstraints(constraints)
       .build()


// Observe Work Progress

WorkManager.getInstance().getWorkInfoByIdLiveData(mathWork.id)
    .observe(this, Observer { info ->
       if (info != null && info.state.isFinished) {
           val myData: Data = info.outputData
           val myResult = myData.getInt(KEY_RESULT, myDefaultValue)
           // ... do something with the result ...
       }
    })


// Work Chaining

WorkManager.getInstance(myContext)
   // Candidates to run in parallel
   .beginWith(listOf(plantName1, plantName2, plantName3))
   // Dependent work (only runs after all previous work in chain)
   .then(cache)
   .then(upload)
   // Call enqueue to kick things off
   .enqueue()

마치며

그냥 예제 보면서 구현했던걸 요즘은 WorkManager로 많이들 구현해서 그렇구나 했던 걸 다시 면밀하게 살펴볼 수 있었다. WorkManager가 정말 다양한 작업들을 지원하는 것도 알 수 있었다.

android sdk 버전이 (api 레벨이) 올라가면서 서비스에 대한 제약도 강력하게 건다고 느꼈는데 이유가 있음을 알게 됐다. 기존부터 엄청 신경쓰던 문제 중 하나라서 그럴 수 밖에 없었던 과거 행적들로 인해 이해도 하게 됐다!

Reference

https://developer.android.com/guide/background

https://developer.android.com/topic/libraries/architecture/workmanager?hl=ko

https://songjinwoo.medium.com/android-background-history-5f93f04f0fdb

profile
와니와니와니와니 당근당근

1개의 댓글

comment-user-thumbnail
2022년 10월 21일

감사합니다 :)

답글 달기