IT_Programming/Android_Java

[펌] Android O에서의 백그라운드 처리를 위한 JobIntentService / 백그라운드 실행 제한

JJun ™ 2017. 6. 26. 18:54



 출처

 : https://medium.com/@cwdoh/android-o에서의-백그라운드-처리를-위한-jobintentservice-250af2f7783c

 : https://developer.android.com/preview/features/background.html?hl=ko



안드로이드의 가장 큰 장점 중 하나는 개발자가 앱 실행 여부와 관계없이 백그라운드에서 시스템 자원을 자유롭게 사용할 수 있다는 점입니다. 그러나 이는 때로 시스템 리소스를 과도하게 소모하는 지름길이 되기도 합니다.

이러한 문제를 해결하기 위해 지난 몇년 간 구글은 안드로이드에서의 배터리 소모를 줄이기 위한 여러가지 작업들을 진행해왔습니다. 이 변경 사항들은 Doze와 App Standby 같이 사용자가 인지하지 못하는 백그라운드 상황에서의 동작 제한 등이 주를 이루고 있습니다. 또한, 배터리나 네트워크 메뉴 등을 통해 사용자가 손쉽게 디바이스에서 앱이 사용한 리소스 수준을 알 수 있도록 기능을 제공하고 있어, 배터리나 네트워크 등의 시스템 리소스를 효율적으로 사용하는 것이 앱의 중요한 지표가 되어가고 있습니다.

Android Nougat에 포함된 2단계의 Doze (Source: Android Developer Site)

안드로이드 O 역시 실질적인 변경 사항은 여러가지를 들 수 있겠지만 그 중에서 가장 큰 영향을 끼치는 변경사항은 이미 적용이 예고되었던 백그라운드 실행 제한과 위치 제한일 것입니다.

백그라운드 실행 제한을 알아보자

이번 글에서는 백그라운드 실행 제한에 해당하는 백그라운드 서비스 실행의 제한과 암시적(Implicit) 브로드캐스트 인텐트의 제한과 이를 해결하는 방안에 대해서 알아보도록 하겠습니다.

백그라운드 서비스 실행 제한

서두에서 말한 바와 같이 백그라운드 서비스는 원하는 시점에 실행할 수 있고 이를 통해 , 사용자가 이를 알아채기 어렵다는 문제가 있습니다. 사용자 입장에서 이야기하자면, 사용자가 앱을 사용하지 않는 시점에도 지속적으로 리소스를 사용하기 때문에 문제가 될 수 있습니다.

Note: 게임은 배터리나 네트워크, 메모리 등을 크게 소모하는 대표적인 장르지만, 사용자들은 이러한 소모가 게임 플레이로부터 일어난다는 사실을 학습하여 알고 있으므로, 이를 문제삼지 않습니다. 하지만 플레이하지 않은 게임이 그러하다면 어떨까요? 이것이 백그라운드에서의 시스템 리소스에 대한 UX라고 할 수 있습니다.




검색해보면 정말 많은 이들이 배터리 소모량에 대해 관심이 많다는 것을 알 수 있습니다.

안드로이드 O에서는 앱이 백그라운드에 진입하게 되면 몇분 뒤 동작 중인 백그라운드 서비스는 자동으로 중지되며 onDestroy()가 호출됩니다. 더하여 백그라운드 상태에서 서비스를 구동하기 위한 startService()의 호출은 IllegalStateException이 발생하며 허용되지 않습니다.


암시적 브로드캐스트 인텐트 제한

암시적 브로드캐스트 인텐트(Implicit Broadcast Intent)란 특정 앱을 대상으로 하지 않는 브로드캐스트 인텐트입니다. 이는 전달대상을 한정하지 않으므로 문제가 됩니다. 이를테면 차량을 빼달라고 하기 위해 아파트 전체에 방송을 하는 것과 마찬가지입니다.


“저기요. 차 빼달래요.” — Implicit Broadcast의 경우 절대로 이런 점잖은 상황은 아닙니다.

이러한 암시적 브로드캐스트 인텐트는 AndroidManifest.xml에 이를 기술하고 있는 모든 Receiver나 Service를 깨울 수 있습니다. 비활성되어 있는 앱 컴포넌트는 처리를 위해 로딩되어야 하고 실행되어야 하며, 이는 분명히 리소스를 소모하는 동작이므로 문제가 될 수 있습니다.

안드로이드 N에서는 이러한 경우를 줄이기 위해 특정 인텐트들에 대한 동작을 제한하는 기능을 소개했습니다. 안드로이드 O는 앱 매니페스트에 있는 암시적 브로드캐스트 수신자를 등록할 수 없도록 하는 더 적극적인 제한을 통해 리소스의 사용을 제한합니다.

targetSDK ≥ 26

이러한 백그라운드 실행 제한은 target SDK가 Android O(API Level 26) 이상인 경우에 적용됩니다. 안드로이드 N 이하 버전을 타겟으로 할 경우 제한없이 서비스 실행이 가능합니다.

하지만, 앱 정보에서 사용자가 강제로 백그라운드 실행 제한을 target SDK와 관계없이 적용할 수 있는 옵션을 제공하고 있으므로 단순히 targetSDK 버전에 따른 코드 구현 형태로는 안심할 수는 없음을 기억해두시기 바랍니다.

Note: 즉, target SDK를 Android N 이하로 설정한 상태에서도 사용자 설정에 따라 startService는 IllegalStateException 예외를 발생할 수 있으며, 브로드캐스트 인텐트가 전달되지 않을 수도 있음을 주의해야 합니다. 이에 대한 더 자세한 내용은 여기를 참조합니다.

그럼에도 불구하고 가능한 것들

앞에서 앱이 백그라운드에 위치한 상태에서 백그라운드 서비스는 예외를 발생하며, 일부 암시적 브로드캐스트 인텐트는 AndroidManifest.xml에 인텐트 필터를 기술하는 형식으로는 수신되지 않음을 이야기 했습니다. 그렇다면 이쯤에서 나올 질문이 있습니다.

“그럼 되는 것은 뭔가요?”

되는 기능들 역시 여전히 많습니다.

포그라운드 서비스의 실행

포그라운드 서비스는 제한없이 사용 가능합니다. 그러나 애초에 백그라운드 상태에서 서비스를 시작할 수 없기 때문에 서비스 시작 후 이를 포그라운드 서비스로 설정하는 것은 불가능한 시나리오가 됩니다.

이는 API 26에 추가된 Context.startForegroundService()를 이용하여 해결 할 수 있습니다. 이 API는 모든 서비스를 제한없이 시작할 수 있게 해주지만, 5초 내에 Service.startForeground()를 통해 Notification과 연결되지 않으면 즉시 해당 서비스를 중지합니다.


미디어 플레이, 다운로드나 업로드는 대표적인 포그라운드 서비스의 사용 사례입니다.

Note: 가이드에는 NotificationManager.startServiceInForeground() API를 이용하여 포그라운드 서비스를 시작할 수 있다고 게시되어 있지만 해당 API는 Developer Preview 2에서 제거되었습니다.

사용자가 이미 기존의 백그라운드 동작을 이해할 수 있거나 적절하게 알림을 통해 전달하는 것이 무리하지 않다면, 안드로이드 O에서 포그라운드 서비스는 매우 좋은 선택이 될 수 있습니다.

암시적 브로드캐스트 예외

다행하게도 모든 암시적 브로드캐스트 인텐트가 제한되는 것은 아니며, 상당수의 필수적인 인텐트는 이전과 같은 방식으로 사용할 수 있습니다. 물론 AndroidManifest.xml에 명시적(Explicit) 브로드캐스트는 등록이 가능한 것은 당연합니다.

동적으로 등록된 리시버를 통한 암시적 브로드캐스트 수신

또한 여전히 Context.registerReceiver()를 통해 리시버를 동적으로 등록할 수 있고 이렇게 등록된 리시버는 암시적 브로드캐스트 인텐트의 수신 제한에 해당되지 않습니다.

JobScheduler를 통한 백그라운드 동작

앞에서 설명한 제한과 허용 기능 사이에서 백그라운드 동작을 생각해보면 아직 부족한 점이 있습니다. 주소록이나 사진과 같이 백그라운드에서 데이터를 동기화하는 작업이 필요한 시나리오를 완전히 배제할 수는 없기 때문입니다. 이러한 경우 JobScheduler를 통해 동작 시나리오를 보완할 수 있습니다.

JobScheduler는 무엇인가

안드로이드 롤리팝에서는 Project Volta와 함께 배터리를 최적화하기 위한 일환으로 JobScheduler를 소개하였습니다. 이는 Job에 필요한 조건 및 인자들(JobInfo)과 해당 조건의 동작(JobService)을 등록하고, 안드로이드 프레임워크에 의해 적정한 실행 시점이 제어되는 백그라운드 실행 기능입니다.

Using the Android Job Scheduler — Google Developers

예를 들어 대용량 데이터 동기화를 위해 ACTION_POWER_CONNECTED 브로드캐스트 인텐트를 수신하고 연결 중인 네트워크 상태나 스토리지를 지속적으로 체크하여야 합니다. JobScheduler를 활용하면 이러한 조건을 가지는 JobInfo 객체를 생성/등록하여 대체할 수 있습니다.

Job 구현은 아래와 같이 3가지 작업으로 구성됩니다.

  1. JobInfo를 통해 Job이 실행될 조건을 설정하고, JobScheduler를 통해 이를 시스템에 등록합니다.
  2. JobService를 상속받아 Job 실행 시 필요한 동작을 구현합니다.
  3. Job 실행을 위한 권한(android.permission.BIND_JOB_SERVICE)를 등록합니다.


Job 실행 조건의 설정과 등록

JobInfo는 네트워크의 연결 상태나 충전 여부, 디바이스의 유휴 시점 등 JobService가 실행되어야 하는 조건을 관리합니다. 설정 가능한 동작 조건은 다음과 같습니다.

  • 연결된 네트워크 타입
  • 충전 여부
  • 디바이스 유휴(Idle) 여부
  • 콘텐트 프로바이더의 갱신
  • 클립 데이터
  • 실행 주기
  • 최소 지연 시간
  • 데드라인 설정
  • 재시도 정책
  • 리부팅 시의 현재 조건 유지 여부

이러한 조건은 단일 조건으로 설정할 수도 있고, 여러개의 조건을 동시에 만족했을 때 Job이 시작되도록 할 수도 있습니다.


class JobSchedulerSample {
    private static final int JOB_ID_UPDATE = 0x1000;
    static void setUpdateJob(Context context) {
        // 대용량 데이터를 업데이트하기 위한 적정 조건 설정
        JobInfo job =
            new JobInfo.Builder(
                // Job에 설정할 Id 값
                JOB_ID_UPDATE,
                // 조건 만족 시 UpdateDataByWiFiService가 실행
                new ComponentName(this, SyncJobService.class)
            )
            // 충분한 저장 공간이 있고,
            .setRequiresStorageNotLow(true)
            // WiFi 등의 비과금 네트워크를 사용 중이며
            .setRequiredNetworksCapabilities(JobInfo.NETWORK_TYPE_UNMETERED)
            // 충전 중 일 때
            .setRequiresCharging(true)
            .build();
        // JobScheduler 서비스
        JobService mJobService = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        // Job을 등록한다.
        mJobService.scheduleJob(job);
    }
}



Job 동작의 처리

Job이 수행할 동작은 JobService를 상속하여 구현합니다. JobService는 시작/종료 시의 동작을 처리할 수 있고, 필요하다면 임의의 시점에 호출하여 Job의 종료를 요청할 수 있는 쉬운 구조를 가지고 있습니다. 이는 다음과 같이 2개의 콜백과 1개의 메소드를 제공합니다.

onStartJob()

Job이 시작될 시점에 시스템에 의해 호출되는 콜백입니다. 단,JobService는 메인스레드에서 실행되므로, 이 지점에서 필요에 따라 Thread나 AsyncTask를 고려해봄직 합니다.

  • onStartJob()의 종료 시 지속할 동작(예를 들면 AsyncTask나 Thread)가 있다면 true, 여기에서 완료된 동작이라면 false를 반환합니다.
  • true를 반환할 경우 이후에 finishJob()의 호출을 통해 명시적으로 작업 종료를 선언할 수 있습니다. 이러한 경우 시스템이 필요에 따라 onStopJob()를 호출하여 작업을 중지할 수 있습니다.

onStopJob()

Job이 종료되기 이전에 취소될 필요가 있을 경우 시스템에 의해 호출되는 콜백입니다.

  • 갑작스러운 중지로 현재 실행하던 Job을 다시 스케쥴러에 등록하여 다음 기회에 실행할 필요가 있다면 true를 반환하여 이를 수행할 수 있습니다.
  • 혹은 false를 반환하여 다시 이 작업이 스케쥴링되는 것을 방지할 수 있습니다.


jobFinished()

Job이 완료되었을 때 호출하여 JobManager로 하여금 해당 Job을 종료하도록 합니다.

  • jobFinished()의 호출은 onStopJob()을 발생시키지 않습니다.


package com.example.android.jobscheduler.service;
import android.app.job.JobParameters;
import android.app.job.JobService;
import android.content.Intent;
public class SyncJobService extends JobService {
    ArtworkDownloader mDownloader;
    @Override
    public void onCreate() {
        super.onCreate();
        mDownloader = ArtworkDownloader.getSequencialDownloader();
    }
    @Override
    public boolean onStartJob(final JobParameters params) {
      // 신규 Job 수행 조건이 만족되었을 때 호출됩니다.
      // onStartJob()의 종료 후에도 지속할 동작이 있다면 true, 여기에서 완료되면 false를 반환합니다.
      // true를 반환할 경우 finishJob()의 호출을 통해 작업 종료를 선언하거나,
      // 시스템이 필요 onStopJob()를 호출하여 작업을 중지할 수 있습니다.
      return mDownloader.hasPendingArtworkDownload();
    }
    @Override
    public boolean onStopJob(JobParameters params) {
      // 시스템에서 Job 종료 시 호출되며, 현재 처리 중인 동작들을 중지해야 합니다.
      // 갑작스러운 중지로 현재 실행하던 Job을 재실행해야 할 경우 true, 새로 스케쥴링을 할 필요가 없다면 false를 반환합니다.
      return !mDownloader.isFinished();
    }
}





JobService에 대한 실행 권한 부여

정상적으로 서비스가 동작하도록 android.permission.BIND_JOB_SERVICE 권한을 AndroidManifest.xml에 부여합니다.

<service
android:name=".SyncJobService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="true"/>
JobScheduler에 대해 궁금한 내용은 Joanna Smith의 “Scheduling jobs like a pro with JobScheduler”를 참조하세요.


일반적인 고려 사항

JobScheduler는 배터리나 메모리 등의 시스템 리소스를 덜 영향을 주며 백그라운드 기능을 사용할 수 있게 하지만, 이것이 만능은 아닙니다. 이를 사용하기 전에 다음 사항들을 기억해두도록 합시다.

  • JobService는 기본적으로 메인스레드에서 동작합니다.
따라서, 비교적 실행시간이 길거나 복잡한 동작이 필요하다면, 새로운 스레드 내지는 비동기적인 동작을 구현할 수 있는 다른 방법을 취해야 합니다.
  • Job의 구분은 id를 통해 이루어집니다.
정적인 id를 사용할 것인지 동적인 id로 이를 관리할 것인지는 시나리오에 따라 다를 것입니다만, 대부분의 경우는 정적인 id로도 충분히 이를 구현할 수 있을 것입니다.
  • Job은 실행 조건을 만족하지 못하는 순간 종료됩니다.
이는 실행 중인 Job의 조건이 복잡할수록 실행과 더불어 유지도 어렵다는 뜻입니다. 배터리의 소모와 실행의 빈도 사이에서 적절한 시나리오를 결정하는 것이 중요합니다.
또한, Job이 중간에 취소되는 경우를 대비하기 위해 트랜잭션(transaction) 형태로 기능을 구현하거나 최종 진행 지점에서 재개가 가능하도록 명확한 중단/재개 시나리오를 가져야 합니다.
  • Job의 실행은 어쨌던 Wakelock을 전제로 합니다.
WakeLock 횟수와 시간은 해당 앱에 대한 배터리 소모 계산에 사용되는 항목입니다. 이는 결국 Job을 너무 자주 발생하게 한다면 사용자에게 모니터링되는 배터리 소모량에서 부정적인 경험을 초래할 수 있습니다. 네트워크나 위치에 대한 갱신 요청 역시 마찬가지입니다. (물론 위치 갱신에 대한 제한은 존재합니다만…)



가장 치명적인 문제는 하위 호환성


물론 JobService를 이용해서 백그라운드 서비스를 구현하는 것은 매우 긍정적인 접근입니다. 그러나, JobService는 기본적으로 API 레벨 21부터 지원되는 기능으로 하위 호환성에 대한 문제가 존재합니다.

특히 안드로이드 O에서 백그라운드 서비스가 원천적으로 차단될 수 있는 환경임을 감안할 때 JobScheduler는 선택이 아닌 필수 사항이 되므로 하나의 APK로 여러 플랫폼 버전들을 지원할 때는 코드 복잡도나 구현 상의 실수를 유발하게 되는 문제가 있습니다.

SupportLib: JobIntentService

Support Library에 추가된 JobIntentService는 이에 대해 보다 편리한 접근 방법을 제공합니다. 이는 안드로이드 O에서는 JobScheduler의 기능을 사용하지만, 그 미만의 플랫폼에서도 백그라운드 서비스로 동작하여 기능을 에뮬레이션하여 실행합니다. JobIntentService는 다음과 같은 특징을 가지고 있습니다.

  • 안드로이드 N 이하의 디바이스에서는 백그라운드 서비스로 동작하며 이를 통해 동일 코드로 하위호환성을 유지할 수 있습니다.
  • 안드로이드 O 이상의 디바이스에서는 JobScheduler를 통해 서비스를 구동합니다. 내부적으로는 jobInfo.setOverrideDeadline(0).build() 로 고정된 Job의 실행 조건에 의해 실행되므로, 이외의 조건은 직접 설정할 수 없습니다.
  • Wakelock을 알아서 관리해주기 때문에 하위 버전의 플랫폼에서 동작할 때 WakefulBroadcastReceiver 를 사용할 필요가 없습니다. 이는 실수로 인한 배터리 소모 등의 이슈를 원천적으로 해결해줍니다.
Background Check and Other Insights into the Android Operating System Framework — Google I/O ’17
Note: 안드로이드 O 이전의 디바이스에서도 사용하거나 보다 상세한 실행 조건이 요구되는 경우에는 JobIntentService 가 아니라 JobScheduler를 이용하여야 합니다. (그에 따른 코드 분기는 선물…이겠죠.)

JobIntentService의 프로젝트 적용

사용 설정

JobIntentService를 사용하기 위해서는 이전의 서포트 라이브러리와 동일하게 build.gradle 파일의 repositories 섹션에 다음과 같은 maven 설정이 포함되어야 합니다.

allprojects {
repositories {
jcenter()
maven {
url "https://maven.google.com"
}
}
}

또한, 다음과 같이 dependencies support-compat 모듈을 추가하여야 합니다. (2017/06/25 기준으로 최신 버전은 26.0.0.-beta2입니다. 최신 버전에 대한 정보는 여기에서 참조하세요.)

dependencies {
...
compile 'com.android.support:support-compat:26.0.0-beta2'
}

JobIntentService는 onHandleWork()를 통해 설정된 Job이 enqueueWork()에 의해 적재된 작업들이 순차적으로 처리되는 부분에서 IntentService와 유사합니다. 단, Job의 수행 조건이 변경되는 경우 onStopCurrentWork()가 호출된다는 점은 유의하여야 합니다.

JobIntentService의 구조

JobIntentService도 다음과 같이 2개의 콜백과 1개의 메소드를 제공하는 간단한 구조를 취하고 있습니다.

enqueueWork (Context context, Class cls, int jobId, Intent work)

실행할 동작을 작업 큐에 추가합니다.

  • 안드로이드 O 이상의 플랫폼에서는 deadline이 0인 상태로 최대한 빠르게 동작할 것을 주문하지만, 단말이 doze 상태에 돌입하거나 메모리 부족 등의 이슈로 인해 실행은 임의의 시간에 시작될 수 있습니다.
  • 안드로이드 N 이하의 플랫폼에서는 startService()를 호출한 것과 동일하게 백그라운드 서비스가 시작되고 즉시 기능을 수행합니다. 이는 doze의 돌입 등과는 무관하게 동작하므로, 실행 시 Doze 등으로 인해 발생하는 네트워크 타임아웃 등은 별도의 이슈로 관리되어야 합니다.
재차 말씀드리지만, JobIntentService가 안드로이드 O 이상에서 JobScheduler를 사용하지만 아래와 같이 Job의 실행 조건이 setOverrideDeadline(0) 로 고정되어 있으므로, 별도의 실행 조건이 필요할 경우에는 JobScheduler를 이용하여 처리하도록 하여야 합니다. 이는 서포트라이브러리 26.0.0-beta2에 포함된 JobIntentService의 디컴파일 코드를 보면 아래와 같이 구현되어 있음을 알 수 있습니다.
@RequiresApi(26)
static final class JobWorkEnqueuer extends JobIntentService.WorkEnqueuer {
private final JobInfo mJobInfo;
private final JobScheduler mJobScheduler;
JobWorkEnqueuer(Context context, Class cls, int jobId) {
...
JobInfo.Builder b = new JobInfo.Builder(jobId, mComponentName);
mJobInfo = b.setOverrideDeadline(0).build();
mJobScheduler = (JobScheduler) context.getApplicationContext().getSystemService(
Context.JOB_SCHEDULER_SERVICE);
}
...
}

onHandleWork(Intent intent)

작업 큐에서 dequeue한 처리 작업을 전달받는 메소드입니다.

  • enqueueWork()에 의해 적재된 인텐트는 서비스의 실행 시 이 메소드로 전달됩니다. IntentService와 마찬가지로 적재된 인텐트는 순차적으로 전달됩니다.
  • Wakelock은 첫번째 인텐트부터 최종 인텐트의 완료까지 자동으로 유지되므로, 실제 작업에 대한 관리만을 수행하면 됩니다.
  • onHandleWork()이 종료될 경우 작업 큐에 적재되어 있는 다음 작업이 이어서 전달됩니다. 좀 더 쉽게 설명하자면 onHandleWork()의 실행 종료가 해당 작업의 종료를 의미하므로, 내부에서는 블로킹 형태로 동작하는 것을 권장합니다.
적재된 작업 큐를 처리하는 로직은 아래와 같이 AsyncTask 내에서 적재된 큐에서 작업을 dequeue하여 onHandleWork()로 전달하도록 구성되어 있습니다. 이 코드 역시 서포트라이브러리 26.0.0-beta2에 포함된 JobIntentService의 디컴파일 코드를 보면 아래와 같이 구현되어 있음을 알 수 있습니다.
final class CommandProcessor extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
GenericWorkItem work;

while ((work = dequeueWork()) != null) {
onHandleWork(work.getIntent());
}

return null;
}

...
}

onStopCurrentWork()

현재 작업이 중지되어야 할 경우 호출됩니다.

  • JobService.onStopJob()과 마찬가지로 시스템에서 Job 종료 시 호출되며, 현재 처리 중인 동작들을 중지해야 합니다.
  • 갑작스러운 중지로 현재 실행하던 작업을 재실행해야 한다면 true를 반환하여 다음 기회에 해당 작업을 재시작할 수 있습니다. 만약 새로 스케쥴링을 할 필요가 없다면 false를 반환하여 작업을 종료할 수 있습니다.

JobIntentService를 통해 앨범 표지를 동기화하는 간단한 예시를 작성해보자면 아래와 같이 구성할 수 있습니다.


import android.content.Context;

import android.content.Intent;
import android.support.v4.app.JobIntentService;
import android.util.Log;
import android.widget.Toast;
import 
/**
 * Example implementation of a JobIntentService.
 */
public class UpdateJobIntentService extends JobIntentService {
    static final int JOB_ID = 1000;
    static final String WORK_DOWNLOAD_ARTWORK = ".DOWNLOAD_ARTWORK";
    ArtworkDownloader mDownloader;
    static void enqueueWork(Context context, Intent work) {
        enqueueWork(context, Update.class, JOB_ID, work);
    }
    @Override
    public void onCreate() {
        super.onCreate();
        mDownloader = ArtworkDownloader.getSequencialDownloader();
    }
    @Override
    protected void onHandleWork(Intent intent) {
      // enqueueWork()에 의해 적재된 인텐트는 여기로 전달됩니다.
      if (WORK_DOWNLOAD_ARTWORK.equals(intent.getAction())) {
          mDownloader.download(intent.getStringExtra("URL"))
      }
    }
    @Override
    public boolean onStopCurrentWork() {
      // 현재 처리 중인 동작들을 중지해야 할 경우 
      return !mDownloader.isFinished();
    }
}

JobIntentService의 코드

그 외의 선택지들

Job 이외에도 적합한 선택지들은 존재합니다. 이에 대한 설명은 Google I/O 2017 “Background Check and Other Insights into the Android Operating System Framework” 세션을 살펴보시기 바랍니다.



Background Check and Other Insights into the Android Operating System Framework — Flowchart


최대한 미루고 모아서 처리하세요.

사실 JobScheduler를 이용하던 하지 않던 간에 중요한 것은 특정한 앱 기능을 사용 중이지 않을 때의 배터리나 메모리의 소모량을 사용자가 인식하는 것은 그렇게 좋은 사용자 경험이 아니라는 점입니다. 가능하면 미루고, 모아서 처리하세요. 이를 손쉽게 구현하는데에는 JobIntentService가 좋은 출발점이 되어 줄 수 있을 것입니다.

이 글에 대한 이상한 점이나 오탈자, 추가 사항 등은 언제든지 댓글이나 메일로 보내주시기 바랍니다. :)









백그라운드 실행 제한



백그라운드에서 실행될 때마다 앱은 기기의 제한된 리소스(예: RAM)를 사용합니다. 이 경우 사용자 환경이 손상될 수 있으며, 게임 재생이나 동영상 보기 등 리소스를 많이 소모하는 앱을 사용 중인 경우에는 특히 그렇습니다. Android O는 사용자 경험을 개선하기 위해 백그라운드에서 실행되면서 앱의 동작을 제한합니다. 이 문서에서는 운영체제의 변경 사항 및 새로운 제한 적용 시 앱이 잘 작동하도록 업데이트하는 방법에 대해 설명합니다.

개요


많은 수의 Android 앱과 서비스를 동시에 실행할 수 있습니다. 예를 들어, 사용자가 현재 창에서 게임을 하면서 다른 창에서 웹을 탐색하고 또한 타사 앱을 사용하여 음악을 재생할 수 있습니다. 동시에 실행되는 앱이 더 많을수록 시스템에 더 많은 부하가 걸립니다. 추가적인 앱이나 서비스가 백그라운드에서 실행 중이면 시스템에 추가적인 부하가 걸리고 사용자 환경이 나빠질 수 있습니다. 예를 들어, 음악 앱이 갑자기 종료될 수도 있습니다.

이러한 문제가 발생할 가능성을 줄이기 위해 Android O는 사용자가 앱과 직접 상호작용하지 않을 때 이 앱이 수행할 수 있는 작업을 제한합니다. Android O를 대상으로 하는 앱은 두 가지 방식으로 제한됩니다.

  • 백그라운드 서비스 제한: 앱이 유휴 상태인 경우 백그라운드 서비스의 사용이 제한됩니다. 이 기능은 사용자에게 잘 보이는 포그라운드 서비스에는 적용되지 않습니다.

  • 브로드캐스트 제한: 제한된 예외의 경우, 앱이 암시적 브로드캐스트에 등록하기 위해 자체 매니페스트를 사용할 수 없습니다. 그렇지만 여전히 앱이 런타임에 브로드캐스트에 등록할 수 있으며, 특정 앱을 대상으로 하는 명시적 브로드캐스트에 등록하기 위해 매니페스트를 사용할 수 있습니다.

대부분의 경우, 앱은 JobScheduler 작업을 사용하여 이러한 제한을 해결할 수 있습니다. 이 접근방식을 통해 앱이 실행되지 않을 때 작업을 수행하면서도, 사용자 환경에 영향을 미치지 않는 방식으로 이러한 작업을 예약할 수 있는 여지를 시스템에 제공할 수 있습니다.

백그라운드 서비스 제한


백그라운드에서 실행 중인 서비스가 기기 리소스를 사용할 수 있으며, 그 결과 사용자 환경이 악화될 가능성이 있습니다. 이러한 문제를 줄이기 위해 시스템은 Android O를 대상으로 하는 앱에 여러 가지 서비스 제한을 적용합니다.

참고: 이 제한은 Android O를 대상으로 하는 앱에만 적용됩니다. API 레벨 25 이하를 대상으로 하는 앱은 영향이 없습니다.

시스템은 포그라운드 앱과 백그라운드 앱을 구분합니다. (서비스 제한 목적의 백그라운드 정의는 메모리 관리에서 사용되는 정의와는 다릅니다. 메모리 관리와 관련된 경우에는 앱이 백그라운드에 있을 수 있으며, 서비스 시작과 관련된 경우에는 앱이 포그라운드에 있을 수 있습니다.) 다음과 같은 경우에는 앱이 포그라운드에 있는 것으로 간주됩니다.

  • 액티비티가 시작되거나 일시 중지되거나 상관없이 보이는 액티비티가 있는 경우
  • 포그라운드 서비스가 있는 경우
  • 앱의 서비스 중 하나에 바인드하거나 앱의 콘텐츠 제공자 중 하나를 사용하여 앱에 또 다른 포그라운드 앱이 연결된 경우 예를 들어 다른 앱이 다음 항목에 바인드되어 있다면 포그라운드에 있는 것입니다.
    • IME
    • 배경화면 서비스
    • 알림 리스너
    • 음성 또는 텍스트 서비스

위의 어떤 조건에도 해당하지 않는 경우 앱은 백그라운드에 있는 것으로 간주됩니다.

앱이 포그라운드에 있는 동안에는 이 앱이 포그라운드 및 백그라운드 서비스를 자유롭게 생성하고 실행할 수 있습니다. 앱이 백그라운드로 이동하더라도 몇 분 정도의 기간 동안은 앱이 서비스를 생성하고 사용하는 것이 여전히 허용됩니다. 이 기간이 끝나면 앱이 유휴 상태로 간주됩니다. 이때 마치 앱이 서비스의 Service.stopSelf() 메서드를 호출한 것처럼 시스템이 앱의 백그라운드 서비스를 중지시킵니다.

어떤 상황에서는 백그라운드 앱이 몇 분 동안 임시 허용 목록에 들어가기도 합니다. 앱이 허용 목록에 있는 동안에는 제한없이 서비스를 시작할 수 있으며, 백그라운드 서비스도 실행이 허용됩니다. 사용자에게 보이는 다음과 같은 작업을 앱이 처리하는 경우 이 앱이 허용 목록에 들어갑니다.

많은 경우 앱이 백그라운드 서비스를 JobScheduler 작업으로 대체할 수 있습니다. 예를 들어, CoolPhotoApp은 백그라운드에서 실행되더라도 사용자가 친구로부터 공유 사진을 수신했는지 여부를 확인해야 합니다. 이전에는 앱의 클라우드 저장소를 통해 확인한 백그라운드 서비스를 사용했습니다. Android O로 마이그레이션하기 위해 개발자는 백그라운드 서비스를 예약된 작업으로 대체합니다. 이 작업은 주기적으로 시작되고 서버를 쿼리한 다음 종료됩니다.

Android O 이전에 포그라운드 서비스를 생성하는 일반적인 방법은 백그라운드 서비스를 생성한 후 이 서비스를 포그라운드로 승격시키는 것이었습니다. Android O에서는 앱이 백그라운드 서비스를 생성할 수 없으면 이 접근방식이 실패합니다. 이러한 이유로 Android O에서는 새로운 메서드NotificationManager.startServiceInForeground()가 도입됩니다. 이 메서드를 호출하는 것은 startService()를 호출하여 백그라운드에서 서비스를 생성한 다음, 즉시 이 서비스의 startForeground() 메서드를 호출하여 서비스를 백그라운드로 승격시키는 것과 동일합니다. 하지만 새로운 서비스는 절대 백그라운드에 있지 않으므로, 백그라운드 서비스 제한 사항이 적용되지 않습니다.

브로드캐스트 제한


앱이 브로드캐스트를 수신하도록 등록한 경우, 앱의 수신기는 브로드캐스트가 전송될 때마다 리소스를 소비합니다. 이 경우 너무 많은 앱이 시스템 이벤트 기반의 브로드캐스트를 수신하도록 등록하면 문제가 될 수 있습니다. 브로드캐스트를 트리거하는 시스템 이벤트로 인해 모든 앱들이 급속하게 리소스를 소비할 수 있으며 이로 인해 사용자 환경이 손상될 수 있습니다. 이러한 문제를 줄이기 위해 Android 7.0(API 레벨 25)에서는 브로드캐스트에 제한을 두었습니다( 백그라운드 최적화에서 설명). Android O는 이러한 제한을 더욱 엄격하게 적용합니다.

참고: 이 제한은 Android O를 대상으로 하는 앱에만 적용됩니다. API 레벨 25 이하를 대상으로 하는 앱은 영향이 없습니다.

  • Android O를 대상으로 하는 앱은 앱 매니페스트에 있는 암시적 브로드캐스트에 대해서는 브로드캐스트 수신기를 더 이상 등록할 수 없습니다. 암시적 브로드캐스트란 특정 앱을 대상으로 하지 않는 브로드캐스트입니다. 예를 들어, ACTION_PACKAGE_REPLACED는 기기상의 일부 패키지가 교체된 사실을 알려주기 위해 등록된 모든 리스너로 전송되므로 암시적 브로드캐스트입니다. 하지만 ACTION_MY_PACKAGE_REPLACED는 패키지가 교체된 앱으로만 전송되므로 암시적 브로드캐스트가 아닙니다. 이때 몇 개의 다른 앱들이 리스너를 해당 브로드캐스트에 등록했는지는 문제가 되지 않습니다.
  • 앱은 자체 매니페스트에 있는 명시적 브로드캐스트에 계속해서 등록할 수 있습니다.
  • 앱은 런타임에 Context.registerReceiver()를 사용하여 임의의 브로드캐스트(암시적 또는 명시적)에 수신기를 등록할 수 있습니다.

많은 경우, 이전에 암시적 브로드캐스트에 등록했던 앱은 JobScheduler 작업을 사용하여 유사한 기능을 가져올 수 있습니다. 예를 들어, 소셜 사진 앱은 가끔씩 데이터 정리가 필요할 수 있으며, 기기가 충전기에 연결되어 있을 때 이 작업을 수행하는 것이 좋습니다. 이전에는 앱이 자체 매니페스트에 있는 ACTION_POWER_CONNECTED에 수신기를 등록했습니다. 앱이 브로드캐스트를 수신할 때 정리가 필요한지 여부를 확인했습니다. Android O로 마이그레이션하기 위해 앱이 해당 매니페스트에서 이 수신기를 제거합니다. 그 대신, 앱은 기기가 유휴 상태이고 충전 중일 때 실행되는 정리 작업을 예약합니다.

참고: 현재는 많은 암시적 브로드캐스트가 이 제한으로부터 제외됩니다. 앱은 자체 매니페스트에서 이러한 브로드캐스트에 수신기를 계속해서 등록할 수 있으며, 앱이 어떤 API 레벨을 대상으로 하는지는 상관없습니다. 제외되는 브로드캐스트 목록은 암시적 브로드캐스트 예외를 참조하세요.

마이그레이션 가이드


이 변경 사항은 API 레벨 25 이하를 대상으로 하는 앱에는 적용되지 않습니다. 앱이 Android O를 대상으로 하는 경우, 새로운 제한 사항을 준수하도록 앱을 업데이트하는 것이 좋습니다.

앱에서 서비스가 어떻게 사용되는지 확인하세요. 여러분의 앱에서 앱이 유휴 상태일 때 백그라운드에서 실행되는 서비스를 사용하는 경우, 이 서비스를 교체해야 합니다. 가능한 해결책은 다음과 같습니다.

  • 앱이 백그라운드에 있는 동안 포그라운드 서비스를 생성해야 하는 경우, 백그라운드 서비스를 생성하고 이 서비스를 포그라운드로 승격시키려고 시도하는 대신 새 NotificationManager.startServiceInForeground() 메서드를 사용합니다.
  • 서비스가 사용자에게 보이는 경우, 이 서비스를 포그라운드 서비스로 만듭니다. 예를 들어, 오디오를 재생하는 서비스는 항상 포그라운드 서비스여야 합니다. startService() 대신 NotificationManager.startServiceInForeground() 를 사용하여 서비스를 생성합니다.
  • 서비스의 기능을 예약된 작업으로 복제하는 방법을 찾습니다. 서비스가 사용자에게 즉시 보이는 작업을 수행하고 있지 않은 경우, 대신 예약된 작업을 사용할 수 있어야 합니다.
  • 네트워크 이벤트 발생 시 선택적으로 애플리케이션을 깨우려면, 백그라운드에서 폴링을 수행하는 대신 FCM을 사용합니다.
  • 애플리케이션이 자연스럽게 포그라운드가 될 때까지 백그라운드 작업을 연기합니다.

앱 매니페스트에 정의된 브로드캐스트 수신기를 검토합니다. 암시적 브로드캐스트에 대한 수신기가 매니페스트에 선언된 경우, 이 수신기를 교체해야 합니다. 가능한 해결책은 다음과 같습니다.

  • 수신기를 매니페스트에 선언하는 대신, 런타임에 Context.registerReceiver()를 호출하여 수신기를 생성합니다.
  • 예약된 작업을 사용하여 암시적 브로드캐스트를 트리거했던 조건을 확인합니다.