WorkManager , 잘 써보기

GM.Lim
14 min readJul 19, 2018

--

Photo by Glenn Carstens-Peters on Unsplash

WorkManager 의 쉬운 사용법을 넘어 이제는 WorkManager 의 상황을 파악하면서 더 잘 사용하는 방법에 대해 알아봅시다.

WorkManager 의 기초 사용법이 궁금하시다면 아래의 포스트를 먼저 읽어주세요.

이번 포스트 에서는 Android Jetpack 의 아키텍처의 구성 요소인 LiveData 에 대한 사전 지식이 약간 필요합니다.

1. 작업 상황 파악

작업이 동작하고 완료 되고 더이상 할일이 없다면 좋겠습니다만 아쉽게도 그런 경우는 별로 없습니다.

완료 된 작업에 대해 사용자에게 알림을 통보해 주거나, 화면에 표시된 프로그래스바를 감춰야 하기도 하고, 사용자의 선택에 따라 이미 처리중인 작업을 취소 해야 하기도 하고, 다시 새로운 작업을 실행 해야 하기도 합니다.

이 경우들을 처리하기 위한 기본은, 현재 내가 추가한 작업의 처리 상태를 파악하는 것입니다.

예시와 함께 설명하겠습니다. 다음과 같은 Worker 클래스를 상속받은 작업자 클래스가 있습니다.

SimpleWorker 는 작업을 시작한 뒤 3초 후 Result.SUCCESS 를 반환하고 작업을 종료 합니다.

이제 이 작업을 WorkManager 를 이용해 수행해 보고 상태 변화를 추적해 보도록 합시다.

WorkRequest 는 처리할 작업인 Worker 와 작업 처리를 위한 정보를 담고 있다고 설명했습니다. WorkRequest 의 객채 생성시 고유한 ID 값을 가지는 UUID 객체가 생성되어 저장됩니다. WorkRequest 의 getId() 메서드를 이용해서 이 UUID 객체를 반환 받을 수 있습니다.

WorkManager 의 getStatusById() 메서드에 이 UUID 객체를 인자로 사용하면 인자값으로 주어진 ID 에 해당하는 작업을 추적할수 있도록 LiveData 객체를 반환합니다.

이 LiveData 객체를 통하여 해당 작업의 상태를 추적할 수 있습니다.

지정된 Observer 에 작업의 상태는 WorkState 객체로 전달됩니다.

WorkState 클래스의 구조는 아래와 같은 구조로 getState() 메서드는 ENQUEUED, RUNNING, SUCCEEDED, FAILED, BLOCKED, CANCELLED 의 6개의 상태를 반환합니다.

이 예시의 로그 출력을 보면 작업 처리 과정에 따라 상태가 Observer 에 전달되면서 State : ENQUEUED -> State : RUNNING -> (3초 후) State : SUCCEEDED 로 출력되는것을 확인할수 있습니다.

2. 작업 간의 정보 전달

이전 포스트에서 이미지를 압축하고, 서버에 업로드 하는 연결된 작업 (Chaining work) 을 처리하는 방법에 대해 알아 보았습니다.

이 작업을 처리하기 위한 과정을 조금더 깊이 생각해봅시다. 사용자가 선택한 이미지를 압축하고, 압축된 이미지를 업로드 작업자에 전달해야 합니다.

압축 작업 에는 어떤 이미지를 압축할것인지를 전달해야 하고, 업로드 작업에는 압축된 이미지가 어떤 것인지 전달해야 합니다.

작업 간에 필요한 정보를 서로 전달하기 위하여 공통으로 쓰이는 단위가 필요합니다. 이를 Data 라고 합니다. Data 는 매우 간단한 클래스로 내부에서 키-값을 가지는 Hash Map 에 전달할 정보를 저장합니다.

Data 클래스의 Hash Map 의 key 는 문자열로 고정되어 있고, value 는 원시값 또는 문자열 또는 배열을 사용할수 있습니다. WorkManager 에 의해 직렬화 (Serializable) 가 가능하지만 그 크기는 10KB 로 제한됩니다.

Data 클래스를 이용하여 압축 작업에 압축할 이미지 이름을 전달해 봅시다.

Data.Builder 를 사용하거나 코틀린의 경우 Map 의 확장함수로 toWorkData() 메서드를 제공하므로 쉽게 Map 객체에서 Data 객체를 생성할수 있습니다.

WorkRequest.Builder 클래스에는 setInputData() 메서드가 존재하고 인자로 Data 객체를 받습니다. 이 메서드를 이용해서 Worker 에 이미지의 이름을 전달할수 있습니다.

Worker 클래스에는 전달받은 Data 객체를 반환하는 getInputData() 메서드가 존재하며 이 메서드를 이용해서 Data 객체를 반환받아 사용합니다. 다음은 CompressWorker 에서 전달받은 파일이름을 로그로 출력하는 예시 입니다.

이제 압축 작업이 끝나고, 업로드 작업을 위해 압축된 이미지의 파일 이름을 UploadWorker 에 전달해 보겠습니다.

작업에서의 입출력 단위는 Data 이므로 이전과 같은 방식으로 Data 객체를 만들고 이를 전달하면 됩니다. Worker 에는 setOutputData() 메서드가 존재하고 이 메서드의 인자로 전달하고자 하는 정보가 담긴 Data 객체를 사용하면 됩니다.

예시는 다음과 같습니다.

UploadWorker 에서 전달받은 정보를 사용하는 방법은 이전 CompressWorker 에서와 같습니다.

3. 여러 작업 에서의 정보 전달

책이 3권 있습니다. 3권의 책에서 가장 많이 나오는 단어를 찾는 작업을 생각해 봅시다. 작업은 각 책에서 가장 많이 나오는 단어를 찾고, 이 결과를 조합해서 다시 정렬 하면 될겁니다.

각 책에서 단어를 찾는 작업의 작업자를 CountWordWorker , 이 결과를 조합해서 정렬하는 작업자를 SortWorker 라고 한다면 3개의 CountWordWorker 에서 결과를 Data 에 count 라는 키로 담아 3개의 Data 객체를 SortWorker 에게 전달합니다. 이때 SortWorker 에 데이터는 어떻게 전달될까요?

결과는 어떠한 CountWordWorker 의 Data 로 덮어써집니다. 어떤 CountWordWorker 의 Data 가 될지는 작업의 처리 상황과 완료 순서에 따라 다르기 때문에 정확히 알수 없습니다.

이렇게 WorkManager 에는 중복되는 key 를 가지는 Data 가 전달되었을때 처리하는 InputMerger 라는 Data 처리자를 가지고 있습니다.

InputMerger 는 OverwritingInputMerger 와 ArrayCreatingInputMerger 의 두가지 타입이 이미 구현되어 있으며, 상속받아 원하는 InputMerger 를 만들어도 됩니다.

WorkManager 의 기본 InputMerger 는 OverwritingInputMerger 입니다.

OverwritingInputMerger 는 여러개의 Data 가 전달될때 같은 key 를 가지는 value 는 덮어씁니다. 데이터 간에 중복 되지 않는 key 의 value 는 새롭게 추가하여 하나의 Data 객체를 만듭니다.

그러므로 위 예시의 경우 각 CountWordWorker 가 각각 count 가 아닌 각자의 key 로 값을 전달한다면 SortWorker 는 모든 값을 다 전달 받을수 있습니다.

하지만 효율적이지 않습니다. 각 CountWordWorker 마다 모두 key 가 다르다는것은 SortWorker 가 모든 키를 알아야 한다는 뜻이고, CountWordWorker 가 추가 될때 마다 SortWorker 도 추가된 key 를 알아야 합니다.

ArrayCreatingInputMerger 는 여러개의 Data 가 전달될때 같은 key 를 가지는 value 를 배열로 전달합니다. 단 배열의 특성상 같은 key 의 value 의타입이 서로 다르다면 배열을 만들수 없기 때문에 Exception 이 발생함을 주의해야 합니다.

위 예시에서는 각 CountWordWorker 에 모두 다른 key 를 사용하는 것보다 ArrayCreatingInputMerger 를 이용해 모든 CountWordWorker 로 부터 count 라는 동일 키로 모든 결과를 배열로 받는것이 더 효율적입니다.

InputMerger 는 WorkRequest.Builder 의 setInputMerger() 메서드를 이용해 지정합니다.

지정하지 않는다면 디폴트 InputMerger 는 OverwritingInputMerger 입니다.

4. 작업 취소

WorkRequest 의 객채 생성시 고유한 ID 값을 가지는 UUID 객체가 생성되어 저장된다고 설명했습니다. 이 ID 는 작업을 취소하기 위해서도 사용됩니다.

WorkManager 의 cancelWorkById() 메서드는 인자로 UUID 객체를 사용하며, 주어진 UUID 값을 가진 작업을 취소합니다.

작업 cacelWork 를 WorkManager 의 큐에 넣고 이 작업을 취소하는 예시는 다음과 같습니다.

작업 별로 자동으로 생성되는 UUID 를 사용하면 작업을 쉽게 제어 할수 있지만 UUID 는 어려운 문자열로 이루어져 있고 작업별로 모두 개별 발행되기 때문에 개발자가 디버깅 하기에 용의하지 않습니다.

이런 문제를 개발자가 직접 부여하는 작업의 이름인 태그로 해결할수 있습니다.

WorkRequest 의 addTag() 메서드를 이용해 작업에 태그를 부여할수 있습니다. 여러개의 태그를 부여해도 됩니다.

작업 마다 모두 다른 태그를 부여하지 않고 같은 태그를 사용해도 됩니다. 이는 곧 그룹 태그의 역할을 하며 작업 취소 시에 해당 태그를 가진 모든 작업을 한번에 취소하는 기능을 하게 됩니다.

태그를 이용해 작업을 취소 하고자 한다면 WorkManager 의 cancelAllWorkByTag() 메서드를 사용합니다. 인자로 주어진 태그를 가지는 작업을 모두 취소합니다.

WorkManager 에서 취소 하고자 하는 작업이 이미 완료 된 작업이라면 취소 메서드는 아무 기능도 하지 않습니다. 아직 실행 전 큐에 담긴 상태라면 실행하지 않고 취소 됩니다.

하지만 이미 실행된 작업을 임의로 멈추지 않습니다. 작업에 대해 멈춰야 한다고 Worker 의 Stopped 플래그 와 Cancelled 플래그를 통하여 알려줍니다. 개발자는 이 플래그를 isStopped() 메서드 와 isCancelled() 메서드를 이용하여 작업 취소에 대비하는 코드를 작성할수 있습니다.

isStopped() 는 작업의 중지, isCancelled() 는 작업의 취소를 뜻합니다. 이에 대한 자세한 설명은 다음과 같습니다.

  • isStopped() == true : 작업이 중지 되었습니다. 작업의 중지는 명시적으로 사용자에 의해 작업 취소가 되었거나, 시스템에 의해 WorkManager 의 처리가 멈추었거나, 해당 작업의 제약조건에 의해 조건이 맞지 않는 상황이 발생하는 경우 중지 됩니다. isCancelled() 가 false 인 경우는 시스템에 의한 작업이 중지된 경우 이므로 이 작업은 다음 어느 시점에 다시 수행될것 입니다.
  • isCancelled() == true : 작업이 취소되었습니다. 사용자 또는 개발자의 필요에 의해 명시적으로 작업이 취소된 경우 이므로 이 작업은 더 이상 재실행 되지 않을것 입니다. 이 플래그 만으로는 의미를 가지지 않으며 , 반드시 isStopped() 가 true 임을 확인 한 후 해당 플래그를 참고 해야 합니다.

이 플래그들이 true 일때 WorkManager 는 해당 작업의 상태에 대해 더이상 Observer 를 통해 처리 상황을 통보하지 않으며 작업 후 반환되는 처리 결과 를 무시합니다.

이 경우 개발자는 Worker 가 작업 중지 또는 취소 이후에 정상적인 결과를 출력 하였는지 파악하기 어렵고 취소 시에 작업 결과를 정리하지 않는다면 WorkManager 의 작업 재실행에 의한 중복 결과물을 만들수도 있습니다.

그러므로 Worker 를 작업할때는 두 플래그 에 대한 적절한 처리를 고려하시기 바랍니다.

5. 유일한 작업, Unique Work

백그라운드 작업을 처리하다보면 빈번이 일어나는 상황이 바로 작업의 관리 입니다.

중복 작업 되지 않아야 하는 상황에서는 같은 작업이 이미 추가되어 있지 않은지 파악해야 하고, 새로운 작업으로 대체 해야 한다면 이전 작업을 찾아서 취소 해야합니다. 이미 추가된 작업 이후에 또다른 작업이 되어야 하는 경우는 이전 작업이 끝날때까지 상황을 추적하여 종료시점에 다시 작업을 추가해야 하죠.

WorkManager 는 친절해서 우리는 작업 처리의 모든 상황을 알수 있고, 제어할수 있지만, 이런 경우가 발생할때마다 매번 새로운 Observer 를 설정하고, 상태를 파악해서 하나하나 동작을 제어하는것은 매우 힘든 일입니다.

이런 케이스들을 위해 WorkManager 는 beginUniqueWork() 메서드로 Unique Work 라는 작업 처리 방식을 제공합니다.

Unique Work 는 작업에 유일한 이름을 부여하고 이 이름을 통해서 큐에 넣거나, 조회하거나, 취소 할수 있습니다.

Unique work 는 같은 이름을 가지는 작업 A 가 이미 WorkManager 의 큐에 있고, 다시 같은 이름의 작업 B 를 큐에 추가 하려고 할때 이전 작업 A 에 대한 현재 작업 B 의 동작 방식을 KEEP, REPLACE, APPEND 의 세가지 중 하나로 지정할수 있습니다.

각 동작방식과 사용법은 다음과 같습니다.

  • KEEP : 작업 A 가 실행 대기 중 이거나 실행 중이면 작업 B 는 WorkManager 의 큐에 추가 되지 않습니다. 작업 A 의 실행이 이미 끝났다면 작업 B 는 큐에 추가 됩니다.
  • REPLACE : 작업 A 를 작업 취소 하고 작업 B 를 큐에 추가 합니다.
  • APPEND : 작업 B 를 BLOCKED 상태로 대기 시킵니다. A 작업이 완료되면 작업 B 를 큐에 추가 합니다.

주의 할점은 REPLACE 의 경우 “4. 작업 취소” 에서 알아본 바와같이 이전 작업A 에 Stopped 플래그 와 Cancelled 플래그를 true 로 전달해 준다는 뜻입니다. 이 두 플래그에 따른 상황 처리는 이전에 설명한것 처럼 개발자의 몫입니다.

6. 마치며

이로서 심화된 WorkManager 의 사용법 까지 알아보았습니다. 그리고 위에 서술된 예제는 다음의 GitHub 저장소에서 다운로드 하실 수 있습니다.

2018년은 안드로이드 가 발표 된지 10주년이 되는 해 입니다. 그리고 구글은 이 10주년을 맞아 구글 I/O 에서 안드로이드 의 개발을 위한 많은 도구 들과 API 를 내놓았습니다.

그중 개인적으로 WorkManager 는 안드로이드에서 개발자에게 혼란했던 이슈 중 하나를 편리하고 쉽게 잘 정리했다고 생각합니다. WorkManager 를 새롭게 공부하고 적용하는데 저의 글이 많은 도움이 되기를 바랍니다.

감사합니다.

7. 참고

  • Google I/O 2018 — Android Jetpack: easy background processing with WorkManager

--

--