본문 바로가기
깡샘 코틀린

15-4 잡 스케줄러

by 농농씨 2023. 7. 8.

앞에서 설명한 대로 앱이 백그라운드 상황일 때 작업 처리에 영향을 받지만 잡 스케줄러를 이용하면 이를 보완할 수 있다. 잡 스케줄러는 안드로이드 5 버전(API 레벨 21)부터 제공했지만 안드로이드 8 버전부터 백그라운드 제약이 생기면서 더 중요해졌다.

 

잡 스케줄러를 이용한다고 해서 모든 상황의 백그라운드 처리를 할 수 있는 것은 아니다. 잡 스케줄러도 개발자가 만드는 서비스이므로 이 서비스를 어떤 상황에서 실행해야 하는지 조건을 명시해 줘야 한다. 이렇게 조건을 명시할 수 있는 상황에서만 백그라운드에서 처리할 수 있다.

 

 

잡 스케줄러의 실행 조건

잡 스케줄러에 조건으로 명시할 수 있는 상황은 다음과 같다.

  • 네트워크 타입
  • 배터리 충전 상태
  • 특정 앱의 콘텐츠 프로바이더 갱신(대표적으로 갤러리 앱)

네트워크 타입이 변경되거나 배터리 충전 상태가 변경되었을 때, 또는 특정 앱의 콘텐츠 프로바이더가 변경되었을 때를 조건으로 명시할 수 있으며 해당 조건에 부합하면 시스템이 잡 스케줄러를 실행한다. 그 밖에 다음과 같은 조건도 명시할 수 있다.

  • 실행 주기
  • 최소 지연 시간
  • 시스템 재구동 시 현재 조건 유지 여부

 

 

잡 스케줄러의 3가지 구성 요소

잡 스케줄러는 다음 3가지 요소로 구성된다.

  • 잡 서비스: 백그라운드에서 처리할 작업을 구현한 서비스이다.
  • 잡 인포: 잡 서비스 정보와 실행될 조건을 지정한다.
  • 잡 스케줄러: 잡 인포를 시스템에 등록한다.

 

잡 서비스-백그라운드 작업 구현

먼저 잡 서비스를 만드는 방법을 살펴보자. 잡 서비스는 개발자가 만드는 서비스이므로 매니페스트에 <service> 태그로 등록한다. 이때 android.permission.BIND_JOB_SERVICE 퍼미션도 포함한다.

// 매니페스트에 잡 서비스 등록
<service
    android:name=".MyJobService"
    android:enabled="true"
    android:exported="true"
    android:permission="android.permission.BIND_JOB_SERVICE"></service>
    // 퍼미션도 등록함

그런 다음 코드에서 JobService를 상속받아 onCreate(), onStartCommand(), onDestroy() 같은 생명주기 함수를 재정의한 클래스를 작성한다.

// 잡 서비스 클래스 작성
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
class MyJobService : JobService() { // 잡 서비스 만들기 위해 JobService클래스를 상속받은 클래스 만듦
	override fun onCreate() { // 생명주기 함수들을 재정의함
    	super.onCreate()
        Log.d("kkang", "MyJobService.......onCreate()")
    }
    override fun onDestroy() {
    	super.onDestroy()
        Log.d("kkang", "MyJobService.......onDestroy()")
    }
    override fun onStartJob(params: JobParamaters?): Boolean { 
    // onStartJob(), 반드시 정의해야함
    // 백그라운드에서 처리할 작업을 구현함
    	Log.d("kkang", "MyJobService.......onStartJob()")
        return false // 작업이 완벽하게 종료되면 false를 반환, 작업이 아직 안끝났으면 true 반환
    }
    override fun onStopJob(params: jobParameters?): Boolean { 
    // onStopJob(), 반드시 정의해야 함
    // 작업이 정상으로 처리되지 않았을 때 호출됨. 이때 이 작업을 다시 시스템에 등록하거나 취소할지를 반환값으로 나타냄.
    	Lob.d("kkang", "MyJobService.......onStobJob()")
        return false // onStopJob()의 반환값이 false이면 잡 스케줄러의 등록을 취소함(true면 재등록)
    }
}

앞에서 서비스의 생명주기를 다룰 때 살펴본 onStartCommand() 함수는 정의할 수는 있지만 호출되지 않으므로 의미가 없다. 그러나 onStartJob(), onStopJob() 함수는 반드시 재정의해야 한다.

 

onStartJob() 함수에는 백그라운드에서 처리할 작업을 구현한다. 이 함수의 반환값은 Boolean 타입인데 true인지 false인지에 따라서 다르게 동작한다.

  • false: 작업이 완벽하게 종료되었음을 의미한다.
  • true: 작업이 아직 끝나지 않았음을 의미한다.

onStartJob() 함수가 false를 반환하면 백그라운드 작업이 완벽하게 끝났음을 의미한다. 그러면 시스템은 더 처리할 필요가 없다고 판단하여 onStopJob() 함수를 건너뛰고 바로 onDestroy() 함수를 호출해 서비스를 종료한다.

위 코드에서는 테스트를 위해 각 함수에서 로그를 출력했다. onStartJob() 함수에서 false를 반환하면 다음과 같은 로그가 출력된다.

MyJobService.......onCreate()
MyJobService.......onStartJob() // onStartJob()에서 false 반환해서
MyJobService.......onDestroy() // 시스템이 onStopJob() 건너뛰고 onDestroy()로 서비스 종료함

onStartJob() 함수가 true를 반환하면 백그라운드에서 처리할 작업이 아직 끝나지 않았다는 것을 의미하며 onDestroy() 함수는 호출되지 않는다. 즉, 오랜 시간 살아있는 서비스가 된다. 따라서 오래 걸리는 작업을 스레드 등에서 처리하고 끝날 때 jobFinish() 함수를 호출하는 식으로 onStartJob() 함수를 구현한다.

 

위 코드에서 onStartJob() 함수를 다음처럼 true를 반환하도록 수정하고 테스트해보자.

// true를 반환하도록 수정하고 테스트
override fun onStartJob(jobParameters: JobParameters): Boolean {
    Log.d("kkang", "JobSchedulatService... onStartJob....")
    Thread(Runnable { // 오래걸리는 작업은 스레드로 처리함
        var sum = 0
        for (i in 1..10) { // 1부터 10까지 누적해서 더함
            sum += i
            SystemClock.sleep(1000)
        }
        Log.d("kkang", "JobSchedularService... onStartJob... thread result : $sum")
        				// 더한 결과를 문자열과 함께 출력함
        jobFinished(jobParameters, false) // 스레드 끝나면 jobFinished() 함수 호출하도록 함
    }).start()
    return true // onStartJob()이 true를 반환함
}

onStartJob() 함수에서 스레드를 이용해 시간이 10초 정도 걸리는 작업을 처리하고 true를 반환했다. 어떤 로그를 출력하는지 확인해 보면 다음과 같다.

MyService.......onCreate()
JobSchedularService... onStartJob....
JobSchedularService... onStartJob... thread result : 55 // 스레드의 작업결과가 출력됨, 아직 서비스 안끝남
MyJobService....... onDestroy() 
// 스레드 작업 끝나고 jobFinished() 함수 호출되고 나서야 onDestroy() 호출되고 서비스 종료됨

onStartJob()  함수는 끝났지만 스레드가 종료될 때까지 서비스는 종료되지 않으며, 스레드에서 명시적으로 jobFinished() 함수가 호출될 때 onDestroy() 함수가 호출되면서 서비스가 종료되었다.

그런데 이때에도 onStopJob() 함수는 호출되지 않았다. onStopJob() 함수가 호출되는 경우는 onStartJob() 함수에서 true를 반환해 서비스가 오랜 시간 살아있는 상황에서 1️⃣갑자기 잡 스케줄러를 실행하는 조건이 변경되거나 2️⃣어디선가 cancel() 함수로 취소했을 때이다. 시스템은 이러한 상황을 잡 스케줄러가 비정상으로 종료된 것으로 인지하고 서비스를 종료하기 전에 처리할 로직을 실행하고자 onStopJob() 함수를 호출한다.

만약 위 코드를 테스트하면서 잡 스케줄러의 실행 조건을 변경하면 다음과 같은 로그가 출력된다.

MyJobService.......onCreate()
JobSchedularService... onStartJob....
MyJobService.......onStopJob() // 잡스케줄러를 실행하는 조건이 변경되어 onStopJob()이 호출됨
MyJobService.......onDestroy() // 서비스 종료됨

 

onStopJob() 함수의 반환값도 Boolean() 타입인데 true, false에 따라 다르게 동작한다.

  • false: 잡 스케줄러 등록을 취소한다.
  • true: 잡 스케줄러를 재등록한다.

onStopJob() 함수는 작업이 정상으로 처리되지 않았을 때 호출된다. 이때 이 작업을 다시 시스템에 등록하거나 취소할지를 onStopJob() 함수의 반환값으로 지정한다.

 

잡 인포-잡 서비스의 실행 조건 정의

잡 서비스를 만들었으면 잡 스케줄러를 이용해 시스템에 등록해야 한다. 이때 잡 서비스가 실행되는 조건JobInfo 객체에 담는다.

// 잡 서비스의 실행 조건 정의
var jobSchedular: JobSchedular? = getSystemService<JobSchedular>()
// 잡 스케줄러를 이용해 등록할 잡서비스의 정보를 잡 인포 객체에 담음. 그래서 잡 인포 객체를 만듦.

JobInfo.Builder(1, ComponentName(this, MyJobService::class.java)).run {
	// 첫 번째 매개변수는 등록할 식별값(여기선 1), 두 번째 매개변수는 등록할 잡 서비스
    setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) 
    // 빌더의 세터함수 이용해 서비스가 실행되는 조건을 명시함
    jobSchedular?.schedule(build()) // 잡 스케줄러로 이 jobInfo 객체를 시스템에 전달함(schedule() 함수 이용)
}

JobInfo.Builder 생성자의 첫 번째 매개변수는 등록할 작업의 식별값이다. 나중에 cancel() 함수로 작업을 취소할 때 이 값을 사용한다. 두 번째 매개변수는 등록할 잡 서비스이다. 이 서비스를 시스템에 등록하겠다는 의미이다. 그리고 빌더의 세터 함수를 이용해 조건을 명시한다. 이렇게 만든 JobInfo 객체를 JobSchedular의 schedule() 함수로 시스템에 등록하면 된다.

 

JobInfo.Builder에 지정한 잡 서비스가 실행되는 조건을 명시할 때는 다음과 같은 세터 함수를 이용한다.

  • setPersisted(true): 기기를 재부팅해도 작업 등록을 유지해야 하는지를 설정한다.
  • setPeriodic(long intervalMillis): 작업의 실행 주기를 설정한다.
  • setMinimumLatency(long minLatencyMillis): 작업의 실행 지연 시간을 설정한다.
  • setOverrideDeadLine(long maxExecutionDelayMillis): 다른 조건에 만족하지 않더라도 작업이 이 시간 안에 실행되어야 함을 설정한다.
  • setRequiredNetworkType(int networkType): 네트워크 타입을 설정한다.
  • setRequiresBatteryNotLow(boolean batteryNotLow): 배터리가 낮은 상태가 아님을 설정한다.
  • setRequiresCharging(boolean requiresCharging): 배터리가 충전 상태인지를 설정한다.

setPersisted() 함수에 true를 전달하려면 매니페스트에 android.permission.RECEIVE_BOOT_COMPLETED 퍼미션을 선언해야 한다. setPeriodic() 함수로 실행주기를 설정할 때는 최소 15분 이상이어야 한다. 그리고 이 함수로 실행주기를 설정하더라도 정확한 시간을 보정하지는 않는다. 주어진 시간 내에 한 번은 반복된다. 이런 세부 사항들이 있다~

 

잡 스케줄러-잡 서비스 등록 시 데이터 전달

잡 서비스를 JobInfo 객체를 이용해 시스템에 등록하면 조건에 만족할 때 실행된다. 이때 잡 서비스에 데이터를 전달하려면 JobInfo.Builder의 setExtras() 함수를 이용한다.

// 잡 서비스에 데이터 전달
var jobSchedular: JobSchedular? = getSystemService<JobSchedular>()
// 데이터 전달 위한 잡 스케줄러 객체 만들기
val extras = PersistableBundle() 
// JobInfo.Builder의 setExtras() 함수의 매개변수로 전달할 PersistableBundle 타입의 객체 생성
extras.putString("extra_data", "hello kkang") // 생성한 객체에 키, 값 형태의 데이터 삽입
val builder = JobInfo.Builder(1, componentName) 
// JobInfo.Builder 생성자의 첫 번째 매개변수는 등록할 작업의 식별값, 두 번째는 등록할 잡 서비스
builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED) // 네트워크 타입 지정하는, 빌더의 세터함수
builder.setRequiresCharging(true) // 충전 상태인지 설정
builder.setExtras(extras) // 빌더의 setExtras 함수로 아까 PersistableBundle타입의 객체 생성해서 넣은 데이터를 전달
val jobInfo = builder.build() // jobInfo 객체 생성, JobInfo.Builder 이용해 담은 정보가 포함되어 있음
jobSchedular!!.schedule(jobInfo) // 잡 스케줄러로 잡 서비스를 등록~

JobInfo.Builder의 setExtras() 함수를 호출하면서 매개변수로 PersistableBundle 타입의 객체를 지정했다. 이 객체에 키-값 형태로 잡 서비스에 전달할 데이터를 담으면 된다.

 

이렇게 전달한 데이터를 잡 서비스에서 가져올 때는 onStartJob() 함수의 매개변수를 이용한다.

// 잡 서비스에 데이터 가져오기
override fun onStartJob(jobParameters: JobParameters): Boolean {
    jobParameters.extras.getString("extra_data") // 매개변수의 extras 속성으로 담은 데이터 가져오기
    (... 생략 ...)
    return false
}

onStartJob() 함수의 매개변수 타입은 JobParameters 이며 이 매개변수의 extras 속성에 전달받은 데이터가 담겨 있다.

'깡샘 코틀린' 카테고리의 다른 글

16-2 안드로이드 기본 앱과 연동하기  (0) 2023.07.09
16-1 콘텐츠 프로바이더 이해하기  (0) 2023.07.08
15-3 백그라운드 제약  (0) 2023.07.07
15-2 바인딩 서비스  (0) 2023.07.07
15-1 서비스 컴포넌트  (0) 2023.07.06