2018년 6월 20일 수요일

[안드로이드] WorkManager에 관하여

Services. The life with/without. And WorkManager.

백그라운드를 다루는 안드로이드의 새 버전들의 출시로 인해, 백그라운드 다루기는 이전보다 더 복잡해졌습니다. 그래서 구글은 JetPack의 일부로, 이러한 백그라운드 다루는 작업을 도와주기 위해 WorkManager를 출시했습니다.
WorkManager가 무엇인지 배우기 전에,이것이 왜 만들어지게 되었고, 왜 필요한지를 알 필요가 있습니다. 어떤 컴포넌트 / 라이브러리든지 그 내부에서 어떤일이 일어나고 있고, 왜 그것을 사용해야 하는지 정확히 아는 개발자가 정말 좋은 개발자입니다.
이 글은 3개의 파트로 구성됩니다.
  1. 메모리 기본
  2. 현재 존재하는 백그라운드 방법
  3. WorkManager
먼저, 백그라운드에 관련된 것을 시작하기 전에, 우리는 안드로이드 프로세스의 메모리 관리의 기본에 대해 이해할 필요가 있습니다. 바로 이것이 첫번째 파트가 될 것입니다.

1. 메모리 기본

아주 오래전에, 안드로이드 커널은 리눅스 커널을 기반으로 해서 개발되었습니다. 안드로이드 커널과 모든 리눅스 커널들의 가장 핵심이 되는 차이점은 바로 스왑공간(Swap Space)가 없다는 것입니다.
리눅스의 스왑공간은 램이 꽉 찼을 때 사용됩니다. 시스템은 더 많은 메모리 리소스를 필요로 하지만 램이 꽉 찼을 때, 메모리상의 비활성 페이지를 스왑공간으로 이동시킵니다. 스왑공간은 램의 용량이 작은 디바이스에 도움이 되긴 하지만, 램의 용량을 늘리는것을 대체할 수는 없습니다.스왑공간은 램보다도 엑세스타임이 느린 하드 드라이브에 위치하기 때문입니다.


안드로이드에서는, 스왑공간 같은 것은 없습니다. 시스템의 메모리가 다 소진되었을 때, OOM 킬러를 이용해 프로세스를 강제종료 시켜버립니다. OOM 킬러는 Visible 상태와 소모된 메모리의 양에 기반하여 프로세시를 정리하여 여유 메모리를 확보합니다.
모든 프로세스는 엑티비티 매니저가 부여한 자신의 oom_adj 점수를 가지고 있습니다. 이 점수는 애플리케이션의 상태(Foreground, Background, Background Service 등등...)의 조합입니다. 다음은 모든 oom_adj값을 보여주고 있습니다.
# Define the oom_adj values for the classes of processes that can be
# killed by the kernel.  These are used in ActivityManagerService.
    setprop ro.FOREGROUND_APP_ADJ 0
    setprop ro.VISIBLE_APP_ADJ 1
    setprop ro.SECONDARY_SERVER_ADJ 2
    setprop ro.BACKUP_APP_ADJ 2
    setprop ro.HOME_APP_ADJ 4
    setprop ro.HIDDEN_APP_MIN_ADJ 7
    setprop ro.CONTENT_PROVIDER_ADJ 14
    setprop ro.EMPTY_APP_ADJ 15
프로세스의 oom_adj의 값이 클수록 커널의 OOM 킬러에게 정리당하기 쉽습니다. OOM킬러는 현재 사용가능한 여유메모리 크기와 oom_adj 임계값을 기반으로 구성한 규칙을 사용합니다. 즉, OOM 킬러의 조건은 다음과 같습니다.
여유 메모리의 공간이 X 보다 작을 때, oom_adj값이 Y보다 큰 프로세스를 정리하라!




즉, 가장 중요한 내용은, 앱이 메모리를 적게 소비할수록, 프로세스가 정리되지 않고 중요한 내용을 다룰 기회가 더 많아진다는 것입니다. 그리고, 두번째로 중요한 내용은 애플리케이션의 상태에 대해 이해하는 것입니다. 그래서, 앱이 백그라운드에 진입했을 때에도 뭔가를 지속적으로 하고싶으면, 서비스 컴포넌트를 사용해야 합니다.
  • 서비스는 UI를 제공하지 않고, 백그라운드에서 오래 걸리는 동작을 수행할 수 있도록 하는 앱 컴포넌트입니다.
다음과 같이 서비스를 사용해야 했던 몇가지 이유가 있습니다.
  1. 시스템에게 이 프로세스가 오래 걸리는 작업이 있음을 알려주고, 그에 맞는 oom_adj 점수를 얻도록 하기 위함입니다.
  2. 안드로이드 애플리케이션의 4대 진입점 중 하나입니다.
  3. 서비스를 별개의 프로세스에서 실행시킬 수 있습니다.
그러나!! 서비스를 사용하는 것의 단점이 있습니다. 프로세스가 계속 실행되고 있는 것이므로 배터리를 엄청나게 소비합니다.
아마 모든 개발자들이 아무런 제한없이 백그라운드에서, 원하는 모든 동작들을 수행했을 것입니다. 그래서, 구글은 마시멜로에서 처음 도즈(Doze)모드를 도입하고 누가에서 더 발전시켰습니다.
만약 도즈모드에 익숙하지 않다면, 반드시 익숙해지길 권합니다. 유저가 디바이스의 스크린을 끄고나면, 도즈모드가 시작되어 네트워크 통신, Sync, GPS, 알람, 와이파이 스캔 등을 비활성화 시켜버립니다. 이 도즈모드는 사용자가 스크린을 켜거나, 디바이스를 충전기에 연결할 때까지 유지됩니다. 중요하지 않은 일을 수행하는 앱의 개수를 줄임으로써 디바이스의 배터리를 절약하도록 합니다.
구글은 도즈모드를 통해 시작한 백그라운드 제한을 오레오에서 더더욱 강화했습니다. API 26으로 타게팅된 앱이 백그라운드 서비스 생성을 허가받지 않은 채로 startService() 메서드를 호출하려고 하면 IllegalStateException 예외를 던집니다.
혹자는 API 26으로 타게팅을 안하면 된다고 생각할 수도 있지만, 구글에서 다음과 같은 정책을 발표했습니다. (정책확인) 요약하자면 다음과 같습니다.
  • 2018년 8월 : 새로 출시되는 앱들은 반드시 API 26(Oreo 8.0) 이상
  • 2018년 11월 : 기존 앱들도 API 26(Oreo 8.0) 이상
  • 2019년 이후 : 매년 targetSdkVersion 요구사항이 향상될 것입니다. 안드로이드가 매년 새로운 버전을 낼 때마다, 모든 앱들은 해당 API 레벨 이상을 타겟팅해야 합니다.
이를 통해 알 수 있는 것은....
백그라운드에서 오랜시간 동작할 수 있다는 서비스의 기본목적을 수행하는 것이 허용되지 않게 되었다. 그러므로, 백그라운드 작업을 위해 더 이상 서비스를 사용하지 않게 될 것이다.
서비스를 대체하여 백그라운드에서 동작을 수행하려면, job API를 사용해야 합니다.

2. 현재 존재하는 백그라운드 방법

약간의 KB를 다운로드하는 간단한 네트워크 예제를 살펴봅시다.
가장 직접적인 방법(그리고 부정확한...)은 해당 동작을 수행하는 별도의 스레드를 가지는 것입니다.
int threads = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(threads);
executor.submit(myWork);
어떤 유저가 매우 느린 3G환경을 가지고 있고, 엘리베이터를 타면서 로그인 동작을 수행했다고 생각해 봅시다. 그리고, 네트워크 작업 중에 유저가 전화를 받게됩니다.
OkHttp의 기본 타임아웃은 꽤 큰 편입니다.
  • connectTimeout = 10_000;
  • readTimeout = 10_000;
  • writeTimeout = 10_000;
게다가, 기본적으로 네트워크의 재시도 회수는 3번입니다.
그러므로, 가장 최악의 경우 90초 (30 * 3)가 걸리게 됩니다.
자 그러면 다음의 문제를 생각해 봅시다.

로그인은 성공했을까요???

앱이 백그라운드에 있는 동안, 우리는 모릅니다. 우리가 위에서 살펴봤듯이, 프로세스가 계속 살아있어서 로그인 요청에 대한 응답을 받아서 유저정보를 잘 저장했을지를 보장할 수 없습니다. 혹, 사용자의 전화가 오프라인 상태가 되어 인터넷 연결이 끊길 수도 있습니다
유저 입장에서, "나는 정보를 입력하고 버튼을 눌렀으니 로그인이 잘 되었을 꺼야" 라고 생각할 수 있습니다. 그렇지만 만약에 로그인이 되어있지 않다면, 유저는 앱의 안좋은 UX를 탓하게 될 것입니다. 그러나, 이는 UX의 문제가 아니라 기술적인 문제입니다.
그래서 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 이상에서만 사용할 수 있습니다. 게다가... API 21, 22에서의 JobScheduler는 버그덩어리 컴포넌트 였습니다. 즉, JobScheduler를 제대로 사용하려면 최소 API 레벨은 23 이여야 한다는 것입니다.
만약 API21, 22에서도 제대로 사용하려면 JobDispatcher를 사용할 수 있습니다.
Job myJob = firebaseJobDispatcher.newJobBuilder()
 .setService(SmartService.class)
 .setTag(SmartService.LOCATION_SMART_JOB)
 .setReplaceCurrent(false)
 .setConstraints(ON_ANY_NETWORK)
 .build();
firebaseJobDispatcher.mustSchedule(myJob);
그러나.... 이것은 구글플레이서비스에 의존성을 가지고 있습니다. 그러므로, 아마존기반의 디바이스, 중극 제조사에서는 사용할 수 없는 방법입니다. 그러므로 JobDispatcher도 좋은 선택은 아닙니다. 그러면, 오레오 이전 디바이스에서, 기존 서비스의 이점을을 취하려면 어떻게 해야 할까요??
바로 서포트 라이브러리의 JobIntentService를 이용하면 됩니다. 이 API는 기존의 인텐트서비스를 job으로 실행할 수 있습니다. 그러나, 이것도 오레오에서 가능한 빨리 호출을 수행하는데 도움이 되지 않습니다. 그러므로, 다시 원점으로 돌아갑니다. 현재 실행중인 기기의 안드로이드 버전에 따른 관리, 백그라운드에서 작업을 실행하고, 적절한 스케쥴러에게 작업 등록 등...

3. WorkManager

위의 내용을 종합했을 때, 디바이스의 안드로이드 버전 / 구글플레이서비스의 여부에 따라 다른 해결책들이 존재합니다. 그러므로, 여러분은 이걸 상황에 맞게 모두 구현해야 한다는 고민에 빠지게 될 것입니다. 안드로이드 팀은 이러한 불만사항을 수용하여 Google I/O에서 WorkManager라는 해결책을 제시했습니다.
WorkManager는 시스템 기반의 백그라운드 프로세싱 API를 제공하여, 구현을 단순화하도록 도와줍니다. 이 클래스는 앱이 포그라운드에 없어도, 백그라운드 Job에서 작업들이 수행되도록 합니다. JobScheduler나 Firebase의 JobDispatcher를 사용할 수 있다면 사용하고, 앱이 포그라운드라면 그 프로세스에서 가급적 처리할 수 있도록 합니다. 개발자가 백그라운드 처리과정을 깊게 신경쓰지 않아도 되도록 간단하게 래핑해놓았습니다.
WorkManager 라이브러리는 몇개의 컴포넌트를 가지고 있습니다.
  • WorkManager : 인수 / 제약조건을 가진 작업을 받아서, 큐에 추가합니다.
  • Worker : 백그라운드 스레드에서 동작하는 doWork() 메서드 하나만 가지고 있습니다. 모든 백그라운드 태스크들은 이 메서드 안에서 수행되어야 합니다. 가능한한 심플하도록 구성해야 합니다.
  • WorkRequest : Worker가 어떤 인수와 제약조건(ex : 인터넷, 충전 등)와 함께 큐에 추가되어야할지를 명시하는 역할입니다.
  • WorkResult : 성공, 실패, 재시도
  • Data : Worker로 주고받는 영구적인 키-벨류 쌍의 값 입니다.
먼저, Worker 클래스를 상속받아 새로운 클래스를 만들고 doWork()메서드를 구현합니다.
public class LocationUploadWorker extends Worker {
    ...
     //Upload last passed location to the server
    public WorkerResult doWork() {
        ServerReport serverReport = new ServerReport(getInputData().getDouble(LOCATION_LONG, 0),
                getInputData().getDouble(LOCATION_LAT, 0), getInputData().getLong(LOCATION_TIME,
                0));
        FirebaseDatabase database = FirebaseDatabase.getInstance();
        DatabaseReference myRef =
                database.getReference("WorkerReport v" + android.os.Build.VERSION.SDK_INT);
        myRef.push().setValue(serverReport);
        return WorkerResult.SUCCESS;
    }
}
다음으로, 생성한 Worker를 실행하기 위해 WorkManager를 호출합니다.
Constraints constraints = new Constraints.Builder().setRequiredNetworkType(NetworkType
            .CONNECTED).build();
Data inputData = new Data.Builder()
            .putDouble(LocationUploadWorker.LOCATION_LAT, location.getLatitude())
            .putDouble(LocationUploadWorker.LOCATION_LONG, location.getLongitude())
            .putLong(LocationUploadWorker.LOCATION_TIME, location.getTime())
            .build();
OneTimeWorkRequest uploadWork = new OneTimeWorkRequest.Builder(LocationUploadWorker.class)
            .setConstraints(constraints).setInputData(inputData).build();
WorkManager.getInstance().enqueue(uploadWork);
WorkManager가 남은 과정들을 모두 처리할 것입니다. WorkManager는 가장 최적의 스케쥴을 선택하여 작업을 큐에 추가할 것입니다. 또한, 작업의 인수, 세부사항, 변경되는 상태를 저장할 것입니다. 심지어, LiveData를 이용하여 작업상태를 구독할 수도 있습니다.
WorkManager.getInstance().getStatusById(locationWork.getId()).observe(this,
        workStatus -> {
    if(workStatus!=null && workStatus.getState().isFinished()){
         ...
    }
});
WorkManager 라이브러리의 구조는 다음의 그림을 참고할 수 있습니다.


  1. 백그라운드로 수행할 작업을 Worker의 doWork() 메서드를 구현하여 정의합니다.
    2.생성한 Worker 인스턴스를 가지고 WorkRequest를 생성합니다. 필요하다면WorkRequest를 생성할 때 DataConstraints를 추가합니다.
  2. WorkManager의 enqueue() 메서드를 이용해 생성한 WorkRequest를 큐에 추가합니다. 이 떄, 내부적으로 Room에 작업을 저장합니다.
  3. 내부적으로 적절한 스케쥴러(JobScheduler, JobDispatcher, Executor, AlarmManager)중 하나를 골라 doWork() 메서드를 호출합니다.
  4. 작업의 결과는 LiveData를 이용해 구독할 수 있고, 해당 작업의 산출물은 Data로 이용할 수 있습니다.
이것은 어디까지나 기본동작으로, 여러 추가기능들이 있습니다. 이 추가기능을 따로 써도 되고, 조합해서도 쓸 수 있습니다.
  • 주기적으로 실행하기
Constraints constraints = new Constraints.Builder().setRequiredNetworkType
        (NetworkType.CONNECTED).build();
PeriodicWorkRequest locationWork = new PeriodicWorkRequest.Builder(LocationWork
        .class, 15, TimeUnit.MINUTES).addTag(LocationWork.TAG)
        .setConstraints(constraints).build();
WorkManager.getInstance().enqueue(locationWork);
  • 두개 이상의 job을 순차적으로 실행하기
WorkManager.getInstance(this)
        .beginWith(Work.from(LocationWork.class))
        .then(Work.from(LocationUploadWorker.class))
        .enqueue();
  • 두개 이상의 job을 병렬로 실행하기
WorkManager.getInstance(this).enqueue(Work.from(LocationWork.class,  LocationUploadWorker.class));
  • Note : 주기적 job과 일회성 job을 함께 조합할 수 없습니다.

이 외에도 WorkManager를 이용해 할 수 있는 많은 상황들이 있습니다. Work 취소하기, 결합하기, 연결하기, 한 Work의 Data를 다른 Work로 합치기 등등... 이 다른 기능들을 더 확인하려면 문서를 찾아보길 권합니다.

원문

https://medium.com/google-developer-experts/services-the-life-with-without-and-worker-6933111d62a6

2018년 6월 5일 화요일

[안드로이드] Android JetPack

Use Android Jetpack to Accelerate Your App Development

이 포스트에서, 안드로이드 앱 개발을 더 쉽도록 도와주는 차세대의 컴포넌트, 도구, 구조적 가이드라인을 포함하는 JetPack을 소개하려고 한다.
JetPack은 새로운 안드로이드의 기능들이 하위호환성을 유지할 수 있도록 도와주는 Support Library로부터 고안되었다. 그리고, 앱의 라이프사이클과 관련된 데이터 처리, 수시로 변하는 데이터 처리의 복잡성을 쉽게 처리할 수 있도록 디자인된 아키텍쳐 컴포넌트를 소개했다. 아키텍쳐 컴포넌트를 작년에 발표한 이래로 많은 앱들이 이 컴포넌트를 사용하기 시작했다. LinkedIn, Zillow, iHeartRadio 같은 회사들은 더 적은 버그, 더 쉬워진 테스트, 앱의 순수 기능에만 집중할 수 있는 시간 등의 이점을 보았다.

What is Android JetPack

Android JetPack은 좋은 앱을 만들기 위한 컴포넌트, 도구, 가이드라인의 모음 이다. JetPack은 기존의 서포트 라이브러리, 아키텍쳐 컴포넌트들을 모두 가져와서 4개의 카테고리로 재배치했다.


JetPack 라이브러리는 안드로이브 플랫폼의 일부로 포함되지 않고(unbundled) 별개로 제공되는 라이브러리 이다. 즉, 각 컴포넌트를 필요한 시점에 맞추어 적욧할 수 있다는 의미다. JetPack에 새로운 기능이 추가되면 손쉽게 앱에 반영하여 배포할 수 있다. JetPack으로 변경되면서,기존/신규 컴포넌트들이 androidx.*로 이동되었다. 해당 내용은 다음의 포스트를 참고한다.
또한, 서포트 라이브러리의 취지대로, JetPack의 컴포넌트도 특정 버전에 종속되지 않도록 만들어졌기 때문에 다양한 안드로이드 버전의 플랫폼에서 실행할 수 있다.
게다가, JetPack은 관심의 분리 / 테스트 가능을 중점적으로 하는 현대 설계기법과 코틀린 통합과 같은 생산성 관련 내용들도 고려하여 제작되었다. 이는 더 적은 코드로 견고하고 고품질의 앱을 쉽게 만들도록 도와준다. JetPack의 컴포넌트들은 유기적으로 동작할 수 있도록 구현되었지만, 개발자는 굳이 모두를 사용할 필요 없이 필요한 부분만 사용할 수도 있다. 그러므로 이미 구현된 모든 부분을 고칠 필요 없이, 필요한 부분에만 JetPack 컴포넌트를 순차적으로 교체할 수도 있다.
우리는 다음과 같은 피드백을 받았기 때문에, JetPack의 이러한 특성이 얼마나 중요한지 알고 있다.
우리 앱은 MVVM으로 코드 구조 변경을 고려하고 있었습니다. 아키텍쳐 컴포넌트는 이 고민을 쉽게 해결하고 구현할 수 있도록 도와주었습니다. 그리고, 테스트하기도 훨씬 수월해졌습니다. ViewModel만 유닛테스트를 수행하면 되기 때문에 코드가 전보다 훨씬 견고해졌습니다.

What's New

다음의 5가지 새로운 컴포넌트가 JetPack에 곧 추가될 것이다.
  • WorkManager alpha release
  • Navigation alpha release
  • Paging stable release
  • Slices alpha release
  • Android KTX (Kotlin Extensions) alpha release

WorkManager

WorkManager 컴포넌트는 백그라운드 제약상황에서 실행이 보장되어야 하는 백그라운드 동작을 구현하기 위한 멋진 해결방법을 제공하는 라이브러리 이다. 이 라이브러리는 다음과 같은 특징을 가지고 있다.
  • 단순함
  • 최근 스타일의 API
  • Google Play Service의 유무에 상관없이 동작할 수 있음
  • work의 그래프를 생성하여 순서를 지정할 수 있음
  • work의 상태를 조회할 수 있음
    자세한 내용은 공식문서에서 확인할 수 있다.

Navigation

엑티비티는 시스템에서 제공하는 앱 UI의 진입점이지만, 화면간에 데이터를 공유할떄나, 화면전환에서 유연성이 떨어지는 점 때문에 이상적인 아키텍쳐라고 볼 수 없었다. 그래서 하나의 엑티비티만을 사용하는 것을 기본 아키텍쳐로 하여 앱을 만들도록 하는 Navagation 컴포넌트가 도입되었다. 프레그먼트의 지원을 통해 Lifecycle, ViewModel 같은 아키텍쳐 컴포넌트의 모든 이점을 얻을 수 있으며, Navigation 컴포넌트가 FragmentTransition의 복잡함을 처리할 수 있다. 게다가, Navigation컴포넌트는 올바른 Up / Back 동작을 자동적으로 빌드되도록, Deep 링크를 지원하도록, NavigationDrawer / BottomNavigation 같은 UI위젯에서도 적절하게 네비게이셔닝이 되도록 도와준다. 안드로이드 스튜디오 3.2에서는 네비게이션 에디터를 추가하여 네비게이션 속성들을 보고 관리할 수 있도록 추가될 예정이다.


자세한 내용은 공식문서에서 확인할 수 있다.

Paging

앱에 표시되는 데이터는 양이 많고, 로드하는 동작은 비용이 크기 때문에, 다운로드나 생성, 한번에 너무 많이 보여주는 동작을 피해야한다. Paging 컴포넌트는 RecyclerView에서 많은 양의 데이터를 로드하고 나타내는 작업을 빠르게 수행할 수 있도록 도와준다. 페이징 된 데이터를 로컬 / 네트워크 / 둘다 에서 불러올 수 있으며, 어떻게 컨텐츠를 불러올지 정의할 수 있다. Room, LiveData, RxJava와 함께 사용될 수 있다.
자세한 내용은 공식문서에서 확인할 수 있다.

Slice

JetPack에 Slice 컴포넌트가 추가되었다. "Slice"는 특정 내용을 다른 앱에서 보여주는 개념이다. 다음의 예시는 구글 어시스턴트의 검색결과로 어시스턴트 UI안에 내용물을 렌더링한다.


자세한 내용은 공식문서에서 확인할 수 있다.

Android KTX

JetPack의 많은 목표중에 하나는 코틀린 언어의 이점을 이용해 앱 구현의 생산성을 높이는 것이다. Android KTX는 코틀린코드를 다음과 같이 변경할 수 있도록 도와준다.
view.viewTreeObserver.addOnPreDrawListener(
  object : ViewTreeObserver.OnPreDrawListener {
    override fun onPreDraw(): Boolean {
      viewTreeObserver.removeOnPreDrawListener(this)
      actionToBeTriggered()
      return true
    }
});
더 간결하게 줄일 수 있다.
view.doOnPreDraw { actionToBeTriggered() }

이는 단지 JetPack을 이용하여 코틀린을 지원하는 시작일 뿐이다. 우리의 목표는 JetPack을 이용해 코틀린 개발자가 좋은 앱을 만드는 것이다.
자세한 내용은 공식문서에서 확인할 수 있다.

원문

https://android-developers.googleblog.com/2018/05/use-android-jetpack-to-accelerate-your.html