안드로이드 코틀린 WorkManager는 어디서 사용하고 어떻게 사용하면될까?
- WorkManager는 왜 필요한가?
- 다른 백그라운드 처리들과의 차이
- WorkManager는 어디에 사용하면될까?
- WorkManager는 어떻게 사용할 수 있는지
WorkManager는 구글에서 안드로이드의 Jetpack의 백그라운드 처리를 도와주기 만든 요소이다.
가장 최근에 나온 기술인 만큼 여러 문제 및 버그를 수정한 상태이며
최근 안드로이드의 백그라운드 작업은 대부분 WorkManager로 할 수 있다고 생각해도 무방하다.
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
WorkManager는 왜 필요할까?
먼저 안드로이드에서 백그라운드로 간 앱이
왜 자동으로 종료되는지 알 필요가 있습니다.
안드로이드에서 메모리 사용
안드로이드 커널은 리눅스 커널을 기반으로 개발되었지만
가장 큰 차이점은 공간 스왑(Swap Space)가 없다는 것이다.
Swap Space란?
- 램이 꽉 찼을 때 사용된다
- 메모리 상의 비활성 데이터를 Swap Space로 이동시킨다
- Swap Space가 램 용량을 늘리는 것을 대체할순 없다
- Swap Space는 액세스 타임이 느린 하드 드라이브에 위치하기 때문이다
안드로이드는 (기본적으로)Swap Space가 없기 때문에
OOM 킬러를 사용해서 프로세스를 강제로 종료시킵니다.
OOM 킬러는 Visible 상태와 소모된 메모리 양에 기반하여 프로세스를 종료하여
메모리를 확보하는 역할을 합니다.
모든 프로세스는 액티비티 매니저가 부여한 자신의 oom_adj 점수를 갖고 있습니다.
이 점수는 앱의 상태(Foreground, Backgound, Service 등)로 결정됩니다.
그리고 OOM 킬러는 다음과 같은 조건으로 프로세스를 종료합니다.
여유 메모리 공간이 x보다 작을 때, oom_adj 값이 y보다 큰 프로세스를 정리해라!
그렇기 때문에
oom_adj의 값이 클수록 OOM 킬러에게 정리당하기 쉽습니다.
즉,
앱이 메모리를 적게 소모할수록 프로세스가 강제 종료될 위험이 적습니다.
두 번째로 이해해야하는 것은
어플리케이션 상태에 대한 이해입니다.
기본적으로 앱이 백그라운드에서 뭔가 지속적으로 하고싶다면
"서비스" 컴포넌트를 사용해야합니다.
서비스란?
UI를 제공하지 않고 백그라운드에서 오래 걸리는 동작을 수행할 수 있도록 하는 앱 컴포넌트
서비스를 사용해야하는 이유는 다음과 같은게 있습니다.
- 시스템에게 해당 프로세스가 오래 걸리는 작업이 있음을 알려준다
- 서비스를 사용하면서 알맞은 oom_adj 점수를 부여한다
- 서비스를 사용하면 별개의 프로세스에서 동작을 실행할 수 있다
- 서비스는 안드로이드 앱의 4대 컴포넌트 중 하나다
여기서 하나의 문제가 있는데
바로 "별개의 프로세스에서 동작을 실행"하는 것입니다.
별도의 프로세스에서 동작을 실행한다는 것은 배터리 소모가 엄청납니다!!!
그렇기 때문에 구글은 "Doze 모드"를 도입하였고 이를 점점 발전시켰습니다.
(= 백그라운드 작업에 제약을 엄청나게 둠)
Doze 모드에 대한 공식 설명 링크
https://developer.android.com/training/monitoring-device-state/doze-standby?hl=ko
구체적으로는 다음과 같이 앱 마켓에 등록할 수 있는 앱에 제약을 하나씩 추가했습니다.
- 2018년 8월 : 새로 출시되는 앱들은 반드시 API 26(Oreo 8.0) 이상
- 2018년 11월 : 기존 앱들도 API 26(Oreo 8.0) 이상
- 2019년 이후 : 매년 targetSdkVersion 요구사항이 향상될 것입니다. 안드로이드가 매년 새로운 버전을 낼 때마다, 모든 앱들은 해당 API 레벨 이상을 타겟팅해야 합니다.
그렇기 때문에 Service를 대체하기 위해 나온게 job API입니다
다른 백그라운드 처리들의 차이점
JobScheduler 의 등장
ComponentName service = new ComponentName(this, MyJobService.class);
JobScheduler mJobScheduler = (JobScheduler)getSystemService(Context.JOB_SCHEDULER_SERVICE);
JobInfo.Builder builder = new JobInfo.Builder(jobId, serviceComponent)
.setRequiredNetworkType(jobInfoNetworkType)
.setRequiresCharging(false)
.setRequiresDeviceIdle(false)
.setExtras(extras).build();
mJobScheduler.schedule(jobInfo);
JobScheduler는 시작된 Job을 스케줄링합니다.
적절한 타이밍에 시스템은 MyJobService를 시작하고 onStartJob() 안에 있는 내용을 실시합니다.
하지만 JobScheduler는 API21 이상에서만 사용할 수 있으며
이후 버그가 너무 많아 JobDispatcher로 대체됩니다.
하지만 JobDispatcher 또한 제대로 동작하지 않는 기기가 있기 때문에
이후 JobIntentService로 대체됩니다.
하지만 JobIntentService 또한 원하는 타이밍에 딱 맞게 동작하지 않는 문제가 있었기 때문에
WorkManager가 등장하게 됩니다.
WorkManager의 등장
위 내용을 볼 때 그럼 개발자들은 Android 버전과 휴대폰 기종 등
다양한 것을 고려해서 백그라운드 서비스를 만들어야하는 문제점을 맞이하게 됩니다.
이러한 문제점을 해결해기 위해서 내놓은 것이 WorkManager입니다.
WorkManager는 다음과 같은 특징을 지닙니다.
- 시스템 기반의 백그라운드 프로세싱 API를 제공하여 구현을 단순화할 수 있도록 도와준다
- 해당 API는 앱이 포그라운드에 없더라도 백그라운드에서 Job이 수행될 수 있도록 한다
- 개발자가 백그라운드 처리과정을 신경쓰지 않아도 되도록 해놨다
- 그렇기 때문에 Service, JobIntentService 등 대부분의 서비스 기능을 대체가능하다
- 짧은 작업, 오래 걸리는 작업 모두 사용가능하다
- 네트워크 상태, 배터리 상태 등으로 트리거가 가능하다.
- 포그라운드에서도 사용가능하다
- FCM과 같이 사용할 수 있다
WorkManager는 다음과 같은 컴포넌트를 갖고 있다
- WorkManager
- 처리해야 하는 작업을 자신의 큐에 넣고 관리한다
- 싱클톤으로 구현이 되어있기 때문에 getInstance()로 WorkManager의 인스턴스를 받아 사용한다
- Worker
- 추상 클래스이며, 처리해야 하는 백그라운드 작업의 처리 코드를 이 클래스를 상속받아 doWork() 메서드를 오버라이드 하여 작성하게 된다
- doWork()
- 작업을 완료하고 결과에 따라 Worker클래스 내에 정의된 enum인 Result의 값중 하나를 리턴해야 한다
- SUCCESS, FAILURE, RETRY의 3개 값이 있으며 리턴되는 이 값에 따라 WorkManager는 해당 작업을 마무리 할것인지, 재시도 할것인지, 실패로 정의하고 중단할 것인지를 결정하게 된다
- doWork()
- 추상 클래스이며, 처리해야 하는 백그라운드 작업의 처리 코드를 이 클래스를 상속받아 doWork() 메서드를 오버라이드 하여 작성하게 된다
- WorkRequest
- WorkManager를 통해 실제 요청하게 될 개별 작업이다
- 처리해야 할 작업인 Work와 작업 반복 여부 및 작업 실행 조건, 제약 사항등 이 작업을 어떻게 처리할 것인지에 대한 정보가 담겨있다
- 반복여부에 따라 onTimeWorkRequest, PeriodicWorkRequest로 나뉜다
- onTimeWorkRequest
- 반복하지 않을 작업, 즉 한번만 실행할 작업의 요청을 나타내는 클래스
- PeriodicWorkRequest
- 여러번 실행할 작업의 요청을 나타내는 클래스
- WorkState
- WorkRequst의 id와 해당 WorkRequest의 현재 상태를 담는 클래스
- 이 WorkState의 상태정보를 이용해서 자신이 요청한 작업의 현재 상태를 파악할 수 있다
- ENQUEUED, RUNNING, SUCCEEDED, FAILED, BLOCKED, CANCLLED의 6개 상태를 가진다
WorkManager 사용하기
Worker 정의하기
무언가를 업로드하는 Worker가 있다고 가장한다
Work에서 실시할 동작은 전부 doWork() 안에 정의한다
class UploadWorker(context:Context, params:WorkerParameters):Worker(context, params) {
companion object{
const val KEY_WORKER = "key_worker"
}
// Worker에서 실시할 작업을 doWork() 안에서 정의
override fun doWork(): Result {
try{
// 넘겨 받은 데이터를 가져온다
val count = inputData.getInt(MainActivity.KEY_COUNT_VALUE, 0)
for (i in 0 until count){
Log.d("UploadWorker", "Uploading $i")
}
val time = SimpleDateFormat("dd/M/yyyy hh:mm:ss")
val currentData = time.format(Date())
// worker가 끝난 이후 반환할 데이터를 정의
val outPutData = Data.Builder()
.putString(KEY_WORKER, currentData)
.build()
// 데이터를 반환함과 동시에 success 처리
return Result.success(outPutData)
}catch (e:Exception){
// worker 결과를 fail 처리
return Result.failure()
}
}
}
MainActivity에서 workManager 정의하고 Work 실행하기
Worker는
한 번만 사용하는 OneTimeWorkRequest
정해놓은 주기로 반복하는 PeriodicWorkRequest
두 가지 종류가 있다.
또한 아래와 같은 동작을 할 수 있다.
- 원하는 데이터를 Bundle과 비슷한 형태로 Worker에 넘겨줄 수 있다
- Work가 끝난 이후 결과와 동시에 결과 값 등을 같이 반환할 수 있다
- Woker와 liveData를 합쳐서 Work의 결과를 observe 할 수 있다
- 여러 Work를 연결해서 순차적으로 실시하거나 한 번에 묶어서 처리할 수 있다.
class MainActivity : AppCompatActivity() {
companion object{
val KEY_COUNT_VALUE = "key_count"
}
private lateinit var binding : ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 일반적인 Worker 실행
binding.button1.setOnClickListener {
setOnOneTimeWorkRequest()
}
// 정해진 기간마다 반복하는 Worker 실행
binding.button2.setOnClickListener {
setPeriodicWorkRequest()
}
}
private fun setOnOneTimeWorkRequest(){
val workManager = WorkManager.getInstance(applicationContext)
// Worker에서 사용할 수 있는 데이터를 넣을 수 있음
val data = Data.Builder().putInt(KEY_COUNT_VALUE, 125)
.build()
// Constraints.Builder()를 통해 실행 제약을 설정할 수 있음
val constraints = Constraints.Builder()
// 네트워크가 연결되어있는 상태일 때
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// OneTimeWorkRequest 생성
val uploadRequest = OneTimeWorkRequest.Builder(UploadWorker::class.java)
// 정의한 제약을 설정
.setConstraints(constraints)
// 사용할 데이터를 넣는다
.setInputData(data)
.build()
// 다수의 OneTimeWorkRequest를 생성
val filteringRequest = OneTimeWorkRequest.Builder(FilteringWorker::class.java)
.build()
val compressingRequest = OneTimeWorkRequest.Builder(CompressingWorker::class.java)
.build()
val downloadingWorker = OneTimeWorkRequest.Builder(DownloadingWorker::class.java)
.build()
// list로 묶거나
val paralleWorks = mutableListOf<OneTimeWorkRequest>()
paralleWorks.add(downloadingWorker)
paralleWorks.add(filteringRequest)
// then을 사용해서 하나씩 선형으로 실행하게 가능함
workManager.beginWith(paralleWorks).then(compressingRequest).then(uploadRequest)
.enqueue() // enqueue로 Work를 실행
// workManager와 liveData를 묶어서 worker의 상태를 observe로 확인 가능
workManager.getWorkInfoByIdLiveData(uploadRequest.id)
.observe(this, Observer {
binding.textView.text = it.state.name
// work의 처리가 끝났을 때
if (it.state.isFinished){
// outputData로 worker가 끝난 이후 결과를 받을 수 있음
// 단, 해당 worker에서 Result에 값을 넣어서 반환해야함
val data = it.outputData
val message = data.getString(UploadWorker.KEY_WORKER)
Toast.makeText(applicationContext, message, Toast.LENGTH_SHORT).show()
}
})
}
// 설정한 주기로 반복하는 worker 정의
private fun setPeriodicWorkRequest(){
val periodicWorkRequest = PeriodicWorkRequest
.Builder(DownloadingWorker::class.java, 15, TimeUnit.MINUTES)
.build()
WorkManager.getInstance(applicationContext).enqueue(periodicWorkRequest)
}
}
'안드로이드(kotlin)' 카테고리의 다른 글
안드로이드 Broadcast Receiver 테스트 하기 (0) | 2023.03.16 |
---|---|
잠금 화면 위에 Activity 열기 - 안드로이드 13 대응 (0) | 2023.02.28 |
안드로이드 코틀린 editText 자동 focus 막기 (0) | 2023.02.14 |
안드로이드 OutOfMemoryError: Java heap space 오류 해결 방법 (0) | 2023.02.04 |
안드로이드 라이브러리 만들고 jitpack으로 배포하기 (0) | 2023.01.29 |
"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
댓글