Android

[Java][Android] WorkManager로 백그라운드 작업 하기

어렵지만 2025. 5. 5. 09:27

개발에 따라 사용자와의 상호작용과 별개로 처리해야하는 작업들이 있습니다.

: 예를들어 서버에서 데이터를 받아오거나, 파일을 저장하거나, 복잡한 연산이 필요한 경우 등등 여러가지 가 있습니다.

 

이런작업들을 화면을 그리는 메인 스레드 (UI 스레드)에서 직접 처리하면 ANR 오류 (애플리케이션이 응답하지 않습니다) 가 발생합니다.

 

고민을 해결하기 위해 Google에서는 Android Jetpack 라이브러리의 일부로 WorkManager를 선보였습니다. 

WorkManager는 백그라운드 작업을 처리할 수 있도록 합니다.

 

1. WorkManager란?

WorkManager는 안드로이드 Jetpack 라이브러리의 핵심 구성 요소 중 하나로, 지연 가능하고(Deferrable) 실행이 보장되는(Guaranteed) 비동기 백그라운드 작업을 쉽게 예약하고 관리할 수 있게 해주는 API입니다.

  • 지연 가능 (Deferrable): 작업이 '즉시' 실행될 필요는 없지만, 언젠가는 반드시 실행되어야 하는 작업을 의미합니다. 예를 들어, 지금 당장 로그를 서버로 보내는 것보다 네트워크가 연결되었을 때 보내는 것이 더 효율적일 수 있습니다.
  • 실행 보장 (Guaranteed): WorkManager에 예약된 작업은 앱이 종료되거나 심지어 기기가 재부팅되더라도, 설정된 실행 조건이 충족되면 결국에는 실행되는 것을 시스템 수준에서 보장합니다.

내부적으로 WorkManager는 기기의 API 레벨에 따라 최적의 방법을 선택하여 작업을 처리합니다.

API 23 이상에서는 JobScheduler를 사용하고, 그 이전 버전에서는 BroadcastReceiver와 AlarmManager를 조합하여 사용합니다. 개발자는 이런 내부 구현을 신경 쓸 필요 없이 일관된 방식으로 WorkManager API만 사용하면 됩니다.

 

2. 언제 사용하면 좋을까요?

  • 주기적인 데이터 동기화: 앱의 로컬 데이터와 서버 데이터를 주기적으로 일치시켜야 할 때 (예: 1시간마다 뉴스 업데이트 확인).
    로그 및 분석 데이터 전송: 사용 통계나 오류 로그 등을 모아두었다가, 기기가 Wi-Fi에 연결되거나 충전 중일 때 서버로 전송할 때.
  • 백그라운드 미디어 처리: 사용자가 앱을 사용하지 않는 동안 이미지 압축, 비디오 인코딩 등의 작업을 수행할 때.
    데이터 미리 가져오기 (Prefetching): 사용자가 다음에 볼 가능성이 높은 콘텐츠(예: 뉴스 기사 본문, 다음 비디오)를 미리 다운로드하여 로딩 시간을 줄이고 싶을 때.
  • 조건부 작업 실행: 특정 조건이 만족되었을 때만 작업을 실행해야 할 때. (예: 특정 위치에 도착하고 + 네트워크가 연결되었을 때 푸시 알림 보내기).

3. WorkManger 왜 사용 해야 할까요?

  • WorkManager를 사용하면 다음과 같은 확실한 이점들을 얻을 수 있습니다.
    앱이 꺼지거나 기기가 재부팅되어도 작업 실행을 보장합니다. 백그라운드 작업 유실 걱정을 크게 덜 수 있습니다.
    똑똑한 제약 조건(Constraints) 지원: 네트워크 상태, 배터리 잔량, 충전 상태, 저장 공간 등 다양한 조건을 설정하여 시스템 리소스(특히 배터리)를 아끼면서 최적의 타이밍에 작업을 실행할 수 있습니다.
  • 안드로이드 API 레벨 14 (아이스크림 샌드위치)까지 지원하여, 파편화 걱정 없이 다양한 기기에서 일관된 방식으로 백그라운드 작업을 처리할 수 있습니다.
  • OneTimeWorkRequest로 일회성 작업을, PeriodicWorkRequest로 주기적인 작업(최소 15분 간격)을 쉽게 예약할 수 있습니다.
    여러 작업을 순서대로(A 완료 후 B 실행) 또는 병렬로 실행하도록 연결하여 복잡한 워크플로우를 쉽게 구성할 수 있습니다. (예: 이미지 다운로드 -> 이미지 필터 적용 -> 서버 업로드)
  • LiveData를 통해 작업의 현재 상태를 실시간으로 관찰하고 UI에 반영하기 매우 편리합니다.
    시스템 최적화: WorkManager는 안드로이드 OS와 긴밀하게 통합되어, 시스템이 배터리 절약 모드 등의 상태를 고려하여 작업을 효율적으로 관리하도록 돕습니다.

4. WorkManger 사용시 단점

  • 즉시 실행 보장 안 됨: WorkManager는 '지연 가능한' 작업을 위한 것입니다. 버튼 클릭 직후 바로 완료되어야 하거나, 앱 프로세스가 살아있는 동안에만 의미 있는 작업(예: 현재 화면과 관련된 빠른 연산)에는 적합하지 않습니다. 이런 경우에는 Kotlin Coroutines, RxJava, 혹은 단순 Thread 등을 사용하는 것이 더 좋습니다
  • "정확히 오후 3시 00분 00초에 실행"과 같은 정밀한 시간 예약 기능은 직접적으로 제공하지 않습니다. 이런 기능이 필요하다면 AlarmManager를 직접 사용하는 것을 고려해야 할 수 있습니다.
  • 최소 반복 간격 제한: PeriodicWorkRequest로 예약하는 주기적인 작업의 최소 간격은 15분입니다. 이보다 더 짧은 주기로 반복해야 하는 작업에는 적합하지 않습니다.
  • Worker, WorkRequest, Constraints, Data 등 WorkManager의 기본 개념들을 이해하는 데 약간의 시간이 필요할 수 있습니다. 하지만 한번 익혀두면 매우 유용합니다.

Sdk 확인

compileSdk 35
dependencies {
    implementation "androidx.work:work-runtime:2.10.1" // 최신 버전 확인!
}

 

LogWorker

import android.content.Context;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class LogWorker extends Worker {
    public static final String TAG = "LogWorker";
    // Worker에게 전달할 데이터의 Key (선택사항)
    public static final String KEY_MESSAGE = "message";
    // Worker가 반환할 데이터의 Key (선택사항)
    public static final String KEY_RESULT_TIMESTAMP = "result_timestamp";

    public LogWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    /**
     * Worker 클래스를 상속받습니다.
     * 생성자는 Context와 WorkerParameters를 받습니다.
     * doWork() 메소드에 실제 백그라운드 로직을 구현합니다. 주의: 이 메소드는 백그라운드 스레드에서 실행됩니다.
     * getInputData(): 작업을 예약할 때 전달된 데이터를 가져옵니다.
     * Result.success(): 작업 성공 시 반환합니다. 선택적으로 Data 객체를 전달하여 결과를 반환할 수 있습니다.
     * Result.failure(): 작업 실패 시 반환합니다.
     * Result.retry(): 작업 재시도가 필요할 때 반환합니다.
     * **/
    @NonNull
    @Override
    public Result doWork() {
        Log.d(TAG, "doWork: 작업을 시작합니다....!");

        // 메인 엑티비티에서 전달받은 데이터 가져오기 (선택사항)
        String message = getInputData().getString(KEY_MESSAGE);
        if (message != null) {
            Log.d(TAG, "전달받은 메시지 : " + message);
        } else {
            Log.d(TAG, "전달받은 메시지 없음... ");
        }

        try {
            //실제 백그라운드를 작업 하는 부분
            // 예: 네트워크 요청, 데이터베이스 저장, 파일처리 등등

            // 여기서는 간단히 5초로 대기하고 로그 남기기
            Log.d(TAG, "doWork: 5초 대기 시작합니다!");
            Thread.sleep(5000);
            Log.d(TAG, "doWork: 5초가 지났습니다...!!!");
            long currentTime = System.currentTimeMillis();
            Log.d(TAG, "doWork: 작업 완료! Timestamp: " + currentTime);

            // 작업 결결과 반환 데이터 담기
            Log.d(TAG, "doWork: 작업 결과을 반환 데이터를 Data 클래스에 담습니다.");
            Data outputData = new Data.Builder()
                    .putLong(KEY_RESULT_TIMESTAMP, currentTime)
                    .build();

            // 작업 성공 시 Result.success() 반환
            // 작업 결과를 포함하여 반환할 수 있습니다.
            Log.d(TAG, "doWork: 반환 데이터를 return합니다.");
            return Result.success(outputData);

        } catch (InterruptedException e) {
            Log.e(TAG, "doWork: 작업 중단됨", e);
            // 작업 실패 시 Result.failure() 반환
            return Result.failure();
        } catch (Exception e) {
            Log.e(TAG, "doWork: 작업 중 오류 발생", e);
            // 작업 실패 시 Result.failure() 반환
            return Result.failure();
        }
    }
}

 

MainActivity

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.lifecycle.Observer;
import androidx.work.Constraints;
import androidx.work.Data;
import androidx.work.NetworkType;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;

import java.util.UUID;

public class MainActivity extends AppCompatActivity {

    Button btnScheduleWork;
    TextView tvStatus;
    WorkManager workManager;
    UUID logWorkRequestId; // 예약된 작업의 ID를 저장할 변수
    String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ConnectLayout();
        ClickMake();
        // workManger 인스턴스 가져오기
        workManagerGet(WorkManager.getInstance(getApplicationContext()));


    }

    void workManagerGet(WorkManager getInstanceItem) {
        workManager = getInstanceItem;
    }

    void ConnectLayout() {
        btnScheduleWork = findViewById(R.id.btn_schedule_work);
        tvStatus = findViewById(R.id.tv_status);
    }

    void ClickMake() {
        btnScheduleWork.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                scheduleLogWork();
            }
        });
    }

    void scheduleLogWork() {
        // 1.  작업에 전달할 데이터 생성
        Data inputData = new Data.Builder()
                .putString(LogWorker.KEY_MESSAGE, "MainActivity에서 보낸 메시지입니다 : 작업에 전달할 데이터를 생성합니다")
                .build();

        // 2. 작업 실행 제약 조건 설정
        Constraints constraints = new Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                // .setRequiresCharging(true) // 충전 중일 때만 실행 (예시)
                // .setRequiresDeviceIdle(true) // 기기가 유휴 상태일 때만 실행 (예시)
                .build();
        Log.d(TAG, "scheduleLogWork: 작업 실행 제약 조건 설정합니다. + setRequiredNetworkType");

        // 3. WorkRequest 생성 (OneTimeWorkRequest 또는 PeriodicWorkRequest)
        // 여기서는 일회성 작업을 생성합니다.
        OneTimeWorkRequest logWorkRequest = new OneTimeWorkRequest.Builder(LogWorker.class)
                .setConstraints(constraints)        // 제약 조건 설정
                .setInputData(inputData)            // 입력 데이터 설정
                // .setInitialDelay(10, TimeUnit.SECONDS) // 10초 지연 후 실행 (예시)
                // .addTag("simple_log_work") // 태그 설정 (여러 작업 관리 시 유용)
                .build();
        Log.d(TAG, "WorkRequest 생성하여 일회성 작업을 생성합니다. +  .setConstraints(constraints) == 제약 조건 설정\n" +
                ".setInputData(inputData) == 입력 데이터 설정 ");

        // 4. WorkManager에 작업 예약
        workManager.enqueue(logWorkRequest);
        Log.i(TAG, "workManager에 작업 예약합니다.");
        // 5. 예약된 작업의 ID 저장 및 상태 관찰 시작
        logWorkRequestId = logWorkRequest.getId();
        tvStatus.setText("작업 상태: 예약됨 (ID: " + logWorkRequestId.toString().substring(0, 8) + ")");
        observeWorkStatus(logWorkRequestId);

        Log.d(TAG, "LogWorker 작업 예약됨. ID: " + logWorkRequestId);
    }

    void observeWorkStatus(final UUID workId) {
        Log.d(TAG, "관찰자 함수가 실행됩니다!!! ");
        workManager.getWorkInfoByIdLiveData(workId)
                .observe(this, new Observer<WorkInfo>() {
                    @Override
                    public void onChanged(WorkInfo workInfo) {
                        if (workInfo != null) {
                            WorkInfo.State state = workInfo.getState();
                            String statusText;

                            switch (state) {
                                case ENQUEUED:
                                    if (isNetworkConnected()) {
                                        statusText = "작업 상태: 예약됨 (실행 대기)";

                                    } else {
                                        statusText = "작업 상태: 예약됨 (네트워크 연결 대기 중) / (네트워크(인터넷)을 연결하면 예약된 작업(LogWork 클래스에 작성한 5초있다가 실행하기)이 수행됩니다)";
                                    }
                                    break;
                                case RUNNING:
                                    statusText = "작업 상태: 실행 중...";
                                    break;
                                case SUCCEEDED:
                                    statusText = "작업 상태: 성공";
                                    Data outputData = workInfo.getOutputData();
                                    // LogWorker의 static 상수 사용
                                    long resultTimestamp = outputData.getLong(LogWorker.KEY_RESULT_TIMESTAMP, 0);
                                    if (resultTimestamp != 0) {
                                        statusText += "\n완료 시간: " + resultTimestamp;
                                    }
                                    break;
                                case FAILED:
                                    statusText = "작업 상태: 실패";
                                    break;
                                case BLOCKED:
                                    statusText = "작업 상태: BLOCKED (네트워크 연결 대기 중 / (네트워크(인터넷)을 연결하면 예약된 작업(LogWork 클래스에 작성한 5초있다가 실행하기)이 수행됩니다)";
                                    Log.w(TAG, "Work ID " + workId + " is BLOCKED. Waiting for network.");
                                    break;
                                case CANCELLED:
                                    statusText = "작업 상태: 취소됨";
                                    break;
                                default:
                                    statusText = "작업 상태: 알 수 없음 (" + state.name() + ")";
                                    break;
                            }
                            Log.i(TAG, "현재 작업 상태 입니다 : " + statusText);

                            statusText = "ID(" + workId.toString().substring(0, 8) + ") " + statusText;

                            tvStatus.setText(statusText);

                            Log.d(TAG, "WorkInfo 변경 (ID: " + workId.toString().substring(0, 8) + "): " + state.name());
                        } else {
                            Log.d(TAG, "WorkInfo 변경 (ID: " + workId.toString().substring(0, 8) + "): workInfo is null");
                        }
                    }
                });
        Log.d(TAG, "observeWorkStatus: Work ID " + workId + " 관찰 시작.");

    }

    // 현재 네트워크 연결 상태 확인 메소드 (AndroidManifest.xml에 권한 추가 필요)
    private boolean isNetworkConnected() {
        ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
        if (cm == null) {
            Log.e(TAG, "ConnectivityManager is null!");
            return false;
        }
        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
        return activeNetwork != null && activeNetwork.isConnectedOrConnecting();
    }
}

 

시연영상