WorkManager로 작업 처리 하기

WorkManager API는 특정 유예가능하며, 비동기 적인 작업들을 쉽게 처리할 수 있게 해줍니다. 이 API들은 개발자들이 작업(Work)을 만들게 하고 WorkManager에게 작업을 넘긴뒤 즉각적으로 또는 적당한 때에 해당 작업을 수행 할 수 있도록 합니다.
예를들면, 앱은 가끔 새로운 리소스를 네트워크로부터 다운로드 받아야하는데, WorkManager를 이용한다면 Task를 설정하고, 실행하기 위한 환결설정을 해줍니다(어떤 작업을 휴대폰이 충전중이면서 네트워크에 연결되었을때만 수행해! 처럼말이죠.)
그리고 WorkManager에게 해당작업을 넘기면 조건이 맞을때  작업을 수행하게 됩니다. 심지어 앱이 강제로 종료되거나 기기가 재부팅 되더라도 작업수행이 여전히 보장됩니다.

 
Note:WorkManager는 마치 서버에 애플리케이션에 관련된 데이터를 업로드 하거나 할때 처럼, 심지어 앱이 종료되더라도 시스템은 하던작업을 계속해서 수행하려고 할 것입니다. 하지만 앱-프로세스가 종료되버린다면 프로세스내에 있던 백그라운드 작업은 안전하게 종료되지 못 할 겁니다. 그럴땐 ThreadPools를 사용하는것을 추천합니다.

WorkManager는 기기의 API레벨과 앱의 상태 등의 요소를 기반으로 하는 작업을 적당한 방법을 찾아 수행합니다. 만약 WorkManager가 앱이 실행중인동안에 여러 태스크중 하나를 실행한다면, WorkManager는 앱프로세스내에서 새로운 스레드를 할당하여 해당 작업을 수행합니다. 만약 앱이 실행중이 아니라면, WorkManager는 기기의 API레벨과 종속성등을 고려하여 JobScheduler, Firebase JobDispatcher 또는 AlarmManager등 적당한 방법으로 백그라운드 태스크를 처리 하게 됩니다. 어떤게 기기가 갖고있는 가용할 수 있는 적당한 API인지 찾고 코드를 짤필요가 없습니다. 대신에 WorkManager에게 그런 작업들을 넘겨버리고 어떤게 제일 좋은방법인지 WorkManager가 고르게 하면 됩니다.
추가적으로 WorkManager는 몇몇 고급 기능들을 제공합니다. 예를들면 하나의 태스크가 끝나면 바로 다음 태스크를 수행한다던가 하는 일련된 작업들을 설정하는것도 가능합니다. 또한 작업수행상태를 확인하고 현재상태를 리턴하여 LiveData가 감지할 수 있게할 수도 있습니다. 작업의 상태를 화면에서 확인하고 싶다면 아주 유용한 기능이 될겁니다.

 
안드로이드 프로젝트에 WorkManager 종속성을 추가하려면 다음 링크를 클릭하세요

각 클래스들의 개념

WorkManager API는 몇몇 클래스들을 사용합니다. 몇가지 경우에서는, 해당클래스를 상속하여 하위클래스로 만든다음 사용하여야 합니다.
주요 클래스의 정보는 이렇습니다.

  • Worker : 어떤 작업을 수행할지 명시합니다. WorkerManager API는 추상클래스인 Worker를 포함하고 있습니다. 개발자는 이 클래스를 상속하여 작업을 수행하면 됩니다.
  • WorkRequest : 개별적인 작업을 나타냅니다. WorkRequest객체는 어떤 Worker클래스가 어떤 작업을 하는지 최소한 명시를 해줘야 합니다. 그러나 또한, WorkRequest 객체한테  어떤 조건에서 동작할건지 같은 세부적인 내용들을 추가해줄 수도 있습니다. 모든 WorkRequest는 자동으로 생성된 고유한 ID를 가지고 있습니다. 이 ID를 이용하여 큐에 배치된 작업을 취소하거나 현재 태스크의 작업상태를 알아낼수도 있습니다. WorkRequest는 추상적인 클래스이기 때문에 OneTimeWorkRequest나 PeriodWorkRequest같은 하위 클래스를 직접 사용할 수 있습니다.
    • WorkRequest.Builder : WorkRequest를 만들수 있게 도와주는 헬퍼 클래스입니다. 다시 말하지만, OneTimeWorkRequest.Builder 또는 PeriodicWorkRequest.Builder 클래스중 하나를 쓰면 됩니다.
    • Constraints : 작업이 실행되야할 시점에 대한 제약조건을 설정합니다. (예를들면 “네트워크가 연결되었을때만 수행해줘!”) 개발자는 Constraints 객체를 Constraints.Builder를 통해 만들고, Constraints를 WorkRequest.Builder에게 WorkRequest가 만들어지기전에 넘겨 줍니다.
  • WorkManager : 요청한 작업을 큐에 넣거나 관리합니다. 개발자는 WorkRequest객체를 WorkManager에게 넘겨 작업이 큐 대기열에 들어갈수 있게 합니다. WorkManager는 시스템자원의 부하를 분산시키는 방식으로 작업을 처리 합니다.
  • WorkStatus : 특정작업에 대한 정보를 포함합니다. WorkManager는 각 WorkRequest 객체에 대한 LiveData를 제공합니다. LiveData는 WorkStatus를 가지고 있으며, 해당 LiveData를 감지하는것으로 개발자는 현재 작업상태가 어떤지 결정할수 있고, 작업이 끝난후에 어떤타입의 반환값이건 받을수 있습니다.

일반적인 작업흐름

사진 라이브러리 앱을 개발한다고 가정하고, 앱은 주기적으로 저장된 사진을 압축하려고 한다. 이럴때 WorkManager API를 사용하여 이미지 압축작업을 수행할 수 있습니다. 이 경우 압축이 일어나는 시점에 대해 개발자가 일일이 다 신경 쓸 필요가 없다. 그냥 작업에 대한 셋팅만 끝내고 잊어버리면된다.
먼저 Worker클래스를 정의해야 하고 doWork()메소드를 재정의 하자. 만들고자 하는 Worker 클래스에 어떤식으로 동작할지에 대한 내용만 명시하도록 하자. 하지만 이 Worker가 언제 어떻게 돌아야하는지에 대한 정보는 적지 말자.

public class CompressWorker extends Worker {
    @Override
    public Worker.Result doWork() {
        // 저장된 이미지를 압축하는 작업은 여기서 하면된다
        // 이 예제에서는 매개변수가없다.
        // 모든 사진을 압축하는것을 전제로 한다.
        myCompress();
        // 성공 또는 실패에 대한 반환값을 알려준다.
        return Result.SUCCESS;
        // (반환값이 RETRY라면 WorkerManager는 작업을 나중에 다시 수행하게된다.
        // Failure라면 다시 수행하지 않는다.)
    }
}

다음은, Worker를 기반으로 하여 OneTimeWorkRequest를 만든다 그런 뒤 WorkManager에게 작업을 queue에 넣어달라고 한다.

OneTimeWorkRequest compressionWork =
        new OneTimeWorkRequest.Builder(CompressWorker.class)
    .build();
WorkManager.getInstance().enqueue(compressionWork);

WorkManager는 작업을 수행할만한 적당한 시간을 선택한다. 시스템의 부하가 어느정도인지 고려하고, 기기가 충전중인지 아닌지 기타등등의 문제를 조율한다. 대부분의 경우에서 제약조건을 명시하지 않는다면, WorkManager는 작업을 즉시 수행하게 된다. 만약 작업상태를 확인하고 싶다면 WorkStatus를 얻으면 된다. 예를들어 작업이 끝났는지 알고 싶다면 다음과 같이 하면된다.

WorkManager.getInstance().getStatusById(compressionWork.getId())
    .observe(lifecycleOwner, workStatus -> {
        // 작업상태에따른 처리는 이곳에서 하면 된다.
        if (workStatus != null && workStatus.getState().isFinished()) {
            // ...
        }
    });

작업 제약조건

만약 원한다면, 언제 작업을 수행하는지 제약조건을 명시할 수도 있다. 예를들어 기기가 idle상태이면서 충전중일때만 수행하고 싶은작업이 있을수 있다. 이런경우 OneTimeWorkRequest.Builder를 만들어야하고 해당 빌더를 통해 OneTimeWorkRequest를 만들도록 하자.

// 언제 작업을 수행할지 대한 Constraints를 만들자.
Constraints myConstraints = new Constraints.Builder()
    .setRequiresDeviceIdle(true)
    .setRequiresCharging(true)
    .build();
// 그런다음 위에서 만든 제약 조건을 이용하여 OneTimeWorkRequest를 만들자
OneTimeWorkRequest compressionWork =
                new OneTimeWorkRequest.Builder(CompressWorker.class)
     .setConstraints(myConstraints)
     .build();

그런다음 아까 말했듯이 WorkManager.enqueue()에게 OneTimeWorkRequest 객체를 넘기자. WorkManager는 개발자가 적은 제약조건들을 살펴보고 언제 수행하면 좋을지 결정한다.

작업 취소하기

큐에 넣은 작업을 취소할수도 있다. 취소하기 위해서는 WorkRequest로 부터 얻은 work ID가 필요하다. 예를들어 아래의 코드는 압축작업을 취소하는 예제이다.

UUID compressionWorkId = compressionWork.getId();
WorkManager.getInstance().cancelWorkById(compressionWorkId);

WorkManager는 최상의 노력으로 주어진 작업을 취소시킨다. 하지만 이것은 본질적으로 불분명하다. 취소를 하려고 할때에 이 작업이 수행중인지 끝났는지 모른다. 또한 WorkManager는 고유한 순차작업 또는 모든 태그가 있는 작업들에 대해서 최선을 다해 모든작업들을 취소시키는 메소드도 제공한다.

고급 기능

WorkManager의 핵심 목적은 간단하게 만들고, 일을 던지고 잊으라는 것이다.
반면에 정교한 작업을 하기위한 고급 기능들을 제공하기도 한다.

반복 작업

반복적으로 수행을 필요로 하는 작업이 있을 수 있다. 예를 들면 사진 관리 앱이 한번만 압축하는것을 원치 않는다면. 더 나아가 사진들을 매번 조사하여 압축이 필요한 새로운 이미지나 변경된 이미지가 있는지 확인하고 싶어할 수 있다. 이런 반복되는 작업은 찾은 이미지를 압축하거나 새로운 “이미지 압축” 작업을 수행할 수도 있다.
반복 작업을 만들기위해서는 PeriodicWorkRequest.Builder클래스를 이용하여 PeriodicWorkRequest를 만들어 OneTimeWorkRequest때처럼 큐에 넣으면 된다. 예를들면, 이미지의 압축여부를 알기위해 PhotoCheckWorker 클래스를 정의했다고 가정하자. 만약 12시간마다 목록에 있는 작업들을 수행한다면 PeriodicWorkRequest를 다음과 같이 만들수 있다.

new PeriodicWorkRequest.Builder photoCheckBuilder =
        new PeriodicWorkRequest.Builder(PhotoCheckWorker.class, 12,
                                        TimeUnit.HOURS);
// 원하면 제약조건을 빌더에서 적용해도 된다
// 실제 작업 객체를 만들자:
PeriodicWorkRequest photoCheckWork = photoCheckBuilder.build();
// 그런다음 반복작업으로 등록하자:
WorkManager.getInstance().enqueue(photoCheckWork);

WorkManager는 개발자가 요청한 간격으로 작업을 실행하려고 시도합니다. 개발자가 지정한 제한 조건 및 기타 요구 사항에 따라 작업이 실행됩니다.

연계 작업

몇몇 작업들은 특정순서를 가지고 돌기도 합니다. WorkManager는 순서있는 여러작업목록을 처리 할 수 있습니다.
예를들면, 3개의 OneTimeWorkRequest인 workA, workB, work3가 있다고 가정합니다. 작업은 반드시 순서에 맞게 동작되어야 합니다. 큐에 넣기 위해서, WorkManager.beginWith() 메소드를 호출하여 순서를 만들고 첫번째 OneTimeWorkRequest를 넘깁니다. 반환값으로 작업순서가 정의되어있는 WorkContinuation이 넘어온다. 그런 다음 남아있는 OneTimeWorkRequest들을 WorkContinuation.then()을 통해 순서대로 추가합니다 . 그런다음 WorkContinuation.enqueue()하면 됩니다.

WorkManager.getInstance()
    .beginWith(workA)
        // Note: WorkManager.beginWith() 메소드는 WorkContinuation 객체를 반환합니다.
        // 아래의 then() 메소드들은 WorkContinuation이 가지고 있는 메소드 입니다.
    .then(workB)    // 참고로 then()은 새로운 WorkContinuation 객체를 반환합니다.
    .then(workC)
    .enqueue();

WorkManager는 각 작업의 제약조건에 따라 큐에 들어간 순서대로 작업을 수행합니다.  만약 어떤 작업이 Worker.Result.FAILURE를 반환한다면 모든 작업이 끝나버립니다.
또한 여러 OneTimeWorkRequest객체를 beginWith() 그리고 then()에 인자값으로 넘길수 있습니다. 만약 여러 OneTimeWorkRequest 객체를 하나의 메소드호출에 넘긴다면, 나머지 queue에 있는 작업들이 수행되기 전에  WorkManager는 방금 넘긴 여러 OneTimeWorkRequest를 병행적으로 수행합니다. 예를 보시죠.

WorkManager.getInstance()
    // 먼저 workA1,workA2,workA3 작업들을 병행처리 합니다.
    .beginWith(workA1, workA2, workA3)
    // 위 작업들이 끝나고나면 workB를 수행합니다.
    .then(workB)
    // 그런다음 workC1, workC2를 다시 순서상관없이 처리 합니다.
    .then(workC1, workC2)
    .enqueue();

WorkContinuation.combin()을 이용한다면, 복합연계로 좀 더 복잡하게 순서를 만들수 있습니다. 예를들면, 순서가 다음과 같다고 가정합니다.

이런 순서를 만들기 위해서는 두가지 분리된 체인을 만들어야 합니다. 그런 뒤 세번째처럼 하나로 병합 시킵니다.

WorkContinuation chain1 = WorkManager.getInstance()
    .beginWith(workA)
    .then(workB);
WorkContinuation chain2 = WorkManager.getInstance()
    .beginWith(workC)
    .then(workD);
WorkContinuation chain3 = WorkContinuation
    .combine(chain1, chain2)
    .then(workE);
chain3.enqueue();

이 경우에는 WorkManager가 workA를 workB보다 먼저 실행시킵니다. 또한 workC를 workD보다 먼저 실행시킵니다. 그런뒤 workB와 workD가 작업이 끝나게 되면 마침내 WorkManager는 workE를 실행합니다.

 
WorkManager가 하위 체인에서 수행되는동안 chain1과 chain2가 순서가 어떤식으로 정해지는지는 보장하지 않습니다. 예를 들면 workB가 workC보다 먼저 또는 후에 실행될 수도 있고, 동시에 실행될수도 있는겁니다. 한가지 보장할 수 있는건 각 하위체인이 가지고 있는 테스크순서는 올바르게 지켜진다는것입니다. workB는 workA가 끝나기 전까지는 실행되지않는다는것이죠.

다양한 숫자의 WorkContinuation 메소드가 있습니다. 이 메소드들은 특정상황들에서 필요한 속기를 제공합니다. 예를들면 WorkContinuation.combine(OneTimeWorkRequest, WorkContinuation…) 메소드가 있고 이 메소드는 WorkManager에게 모든 WorkContinuation 체인을 완료하라고 명령합니다. 그런뒤 OneTimeWorkRequest를 끝냅니다. 자세한 정보는 WorkContinuation을 확인하세요.

고유한 작업 순서

어떤 WorkRequest객체건 tag를 적용하는것으로 인해 작업들을 논리적으로 그룹지을수 있습니다. tag를 적용하기 위해서는 WorkRequest.Builder.addTag()를 호출합니다. 예를 들면,

OneTimeWorkRequest cacheCleanupTask =
        new OneTimeWorkRequest.Builder(MyCacheCleanupWorker.class)
    .setConstraints(myConstraints)
    .addTag("cleanup")
    .build();

WorkManager클래스들은 여러가지 유틸리티 메소드들은 제공하는데 개발자가 모든 작업을 특정 태그를 이용하여 수행할 수 있게 합니다. 예를들면, WorkManager.cancelAllWorkByTag()는 특정 태그를 갖고 있는 모든 작업들을 취소시킵니다. 그리고 WorkManager.getStatusesByTag()는 해당 태그를 가지고 있는 WorkStatus 리스트를 반환하게 됩니다.

인자 입력하고 값 반환하기

높은 유연성을 위해서는 개발자는 작업에 대해 인자를 넘기고 반환되는 값을 받을수 있어야 합니다. 값을 넘기고 받는건 키-값 쌍으로 합니다. 인자를 넘기기 위해서 WorkRequest.Builder.setInputData() 메소드를 WorkRequest객체를 만들기전에 호출합니다. 이 메소드는 Data.Builder를 통해 만들어진 Data를 인자로 받습니다. Worker클래스는 Worker.getInputData()를 호출함으로써 이러한 인자들에 접근할수 있게 됩니다. 값을 반환하기 위해서는 Data객체를 인자로 받는 Worket.setOutputData()를 호출합니다. LiveData<WorkStatus>를 observing하는것으로 산출물을 받아 볼 수 있습니다.
예를들면 어떤 계산 작업을 수행하는 Worker 클래스가 있다고 가정하고, 아래의 코드르 확인합시다.

public class MathWorker extends Worker {
    // 파라미터 키값 정의
    public static final String KEY_X_ARG = "X";
    public static final String KEY_Y_ARG = "Y";
    public static final String KEY_Z_ARG = "Z";
    // 결과 키값 정의
    public static final String KEY_RESULT = "result";
    @Override
    public Worker.Result doWork() {
        // 인자값을 가지고 오기 위해서는 아래와 같이 해야하며, 기본값을 정의해줘야 한다.
        int x = getInputData().getInt(KEY_X_ARG, 0);
        int y = getInputData().getInt(KEY_Y_ARG, 0);
        int z = getInputData().getInt(KEY_Z_ARG, 0);
        // 계산작업을 한다.
        int result = myCrazyMathFunction(x, y, z);
        //결과를 출력하면 끄-읕!
        Data output = new Data.Builder()
            .putInt(KEY_RESULT, result)
            .build();
        setOutputData(output);
        return Result.SUCCESS;
    }
}

작업을 만들고 인자를 넘기기 위한 코드는 이렇게 하면된다.

// Data객체 만들기
Data myData = new Data.Builder()
    // X,Y,Z 3가지 인자 넘기기
    .putInt(KEY_X_ARG, 42)
    .putInt(KEY_Y_ARG, 421)
    .putInt(KEY_Z_ARG, 8675309)
    //build()를 통해 실제 Data 객체 만들기:
    .build();
// 그런다음 myData가 포함된 OneTimeWorkRequest를 만들고 큐에 넘는 작업을 한다
OneTimeWorkRequest mathWork = new OneTimeWorkRequest.Builder(MathWorker.class)
        .setInputData(myData)
        .build();
WorkManager.getInstance().enqueue(mathWork);

들어오는 결과값은 WorkStatus로 확인 가능하다.

WorkManager.getInstance().getStatusById(mathWork.getId())
    .observe(lifecycleOwner, status -> {
         if (status != null && status.getState().isFinished()) {
           int myResult = status.getOutputData().getInt(KEY_RESULT,
                  myDefaultValue));
           // ... do something with the result ...
         }
    });

만약 작업들을 묶고자 한다면, 하나의 작업에서 넘어오는 반환값을 다음 작업의 인자값으로 쓸수도 있다. 하나의 OneTimeWorkRequest 뒤에 하나의 OneTimeWorkRequest가 있는 간단한 연계작업인 경우 첫번째 작업은 setOutputData()를 호출하여 결과값을 반환하고 다음 작업은 getInputData()를 호출하여 그 결과를 가져옵니다. 여러 작업이 모두 하나의 작업에게 결과값을 보내는 복잡한 작업이라면 OneTimeWorkRequest.Builder에서 InputMerger를 정의하여 다른 작업이 동일한 키를 사용하여 출력을 반환 할 경우 수행 할 작업을 지정할 수 있습니다.

카테고리: 미분류

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.