Android

[Java][Android] RXJava 사용하기 / AsyncTask의 대체

어렵지만 2025. 7. 13. 15:25

AsyncTask의 대체

안드로이드 개발에서 비동기 처리는 사용자 경험을 해치지 않고 백그라운드에서 시간이 오래 걸리는 작업(네트워크 통신, 데이터베이스 접근 등)을 처리하는 데 필수적입니다. 과거에는 AsyncTask가 이러한 비동기 처리를 위한 손쉬운 방법으로 널리 사용되었습니다. 하지만 AsyncTask는 몇 가지 중요한 설계상의 단점(예: 메모리 누수 위험, 유연성 부족, 복잡한 스레드 관리의 어려움) 때문에 더 이상 권장되지 않습니다.

 

https://developer.android.com/reference/android/os/AsyncTask

 

AsyncTask  |  API reference  |  Android Developers

 

developer.android.com

 

따라서 안드로이드는 Kotlin Coroutines와 함께 RxJava 비동기 프로그래밍의 대안으로 말하고 있습니다.

 

간단한 예제를 통해 구조를 파악해 보겠습니다

 

의존성 추가

dependencies {
    implementation libs.rxjava
    implementation libs.rxandroid

}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:gravity="center"
    android:padding="16dp"
    tools:context=".MainActivity">

    <ProgressBar
        android:id="@+id/progressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:visibility="gone" /> <!-- 초기에는 숨김 -->

    <TextView
        android:id="@+id/textViewResult"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="여기에 결과가 표시됩니다."
        android:textSize="18sp"
        android:layout_marginTop="16dp"
        tools:text="데이터 로딩 완료!" />

    <Button
        android:id="@+id/buttonLoadData"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="데이터 로드 시작"
        android:layout_marginTop="24dp" />

</LinearLayout>

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

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 io.reactivex.Observable;
import io.reactivex.Observer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;

public class MainActivity extends AppCompatActivity {

    // UI 요소들에 대한 참조 변수
    private TextView textViewResult;
    private ProgressBar progressBar;
    private Button buttonLoadData;
    String TAG = "MainActivity";

    // RxJava 구독을 관리하기 위한 CompositeDisposable 객체.
    // Activity가 종료될 때 모든 구독을 해제하여 메모리 누수를 방지합니다.
    private CompositeDisposable disposables = new CompositeDisposable();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // activity_main.xml 레이아웃을 현재 액티비티의 콘텐츠 뷰로 설정합니다.
        setContentView(R.layout.activity_main);

        // XML 레이아웃에서 UI 요소들의 참조를 가져옵니다.
        textViewResult = findViewById(R.id.textViewResult);
        progressBar = findViewById(R.id.progressBar);
        buttonLoadData = findViewById(R.id.buttonLoadData);

        // "데이터 로드 시작" 버튼에 클릭 리스너를 설정합니다.
        buttonLoadData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 버튼 클릭 시 비동기 작업을 시작하는 메서드를 호출합니다.
                performAsyncTaskWithRxJava();
            }
        });
    }

    /**
     * RxJava를 사용하여 비동기 작업을 수행하고 결과를 UI에 표시하는 메서드입니다.
     */
    private void performAsyncTaskWithRxJava() {
        // 1. UI 업데이트: 작업 시작 전에 로딩 상태를 표시합니다.
        showLoadingState();

        // 2. Observable 생성: 비동기적으로 수행될 작업을 정의합니다.
        //    Observable.create()는 직접 Observable을 생성할 때 사용됩니다.
        Observable<String> dataObservable = Observable.create(emitter -> {
            // emitter: Observable이 데이터를 발행하거나 완료, 오류를 알리는 인터페이스
            try {
                // ====== 백그라운드 스레드에서 실행될 핵심 로직 ======
                // 실제 네트워크 요청, 데이터베이스 접근, 복잡한 계산 등이 여기에 들어갑니다.
                // 이 예제에서는 3초간 대기하여 비동기 작업이 수행되는 것을 시뮬레이션합니다.
                System.out.println("RxJava 작업 시작 - Thread: " + Thread.currentThread().getName());

                Thread.sleep(3000); // 3초 동안 작업 수행 시뮬레이션

                String result = "네트워크에서 데이터를 성공적으로 로드했습니다!";
                emitter.onNext(result); // 작업 결과를 발행합니다. (Observer의 onNext 콜백으로 전달됩니다.)
                emitter.onComplete();   // 작업이 성공적으로 완료되었음을 알립니다. (Observer의 onComplete 콜백으로 전달됩니다.)
                // ==================================================

            } catch (InterruptedException e) {
                // 작업 중 InterruptedException (예: 쓰레드 중단)이 발생하면 오류를 발행합니다.
                emitter.onError(e); // Observer의 onError 콜백으로 전달됩니다.
            } catch (Exception e) {
                // 기타 예상치 못한 예외가 발생하면 오류를 발행합니다.
                emitter.onError(e); // Observer의 onError 콜백으로 전달됩니다.
            }
        });

        // 3. RxJava 연산자 체이닝: 스케줄러 설정 및 구독
        dataObservable
                // subscribeOn(): Observable의 작업(create 내부 로직)이 실행될 스레드를 지정합니다.
                // Schedulers.io()는 I/O 바운드 작업(네트워크, 파일 입출력 등)에 적합한 스레드 풀을 사용합니다.
                .subscribeOn(Schedulers.io())

                // observeOn(): 발행되는 데이터(onNext)가 전달될 스레드를 지정합니다.
                // AndroidSchedulers.mainThread()는 Android의 UI 스레드를 나타냅니다.
                // UI 업데이트는 반드시 메인 스레드에서 이루어져야 하므로 여기서 사용합니다.
                .observeOn(AndroidSchedulers.mainThread())

                // subscribe(): Observable을 구독하고 데이터를 수신합니다. Observer 객체를 인자로 받습니다.
                .subscribe(new Observer<String>() {
                    @Override
                    public void onSubscribe(Disposable d) {
                        // 구독이 성공적으로 시작되었을 때 호출됩니다.
                        // 이 메서드는 옵저버가 연결된 직후에 한 번만 호출됩니다.
                        System.out.println("RxJava 구독 시작됨 - Thread: " + Thread.currentThread().getName());

                        // Disposable 객체를 CompositeDisposable에 추가하여 구독을 관리합니다.
                        // 이렇게 하면 나중에 disposables.dispose() 호출 시 모든 구독이 한 번에 취소됩니다.
                        disposables.add(d);
                    }

                    @Override
                    public void onNext(String s) {
                        // Observable이 onNext(data)를 호출하여 데이터를 발행할 때마다 호출됩니다.
                        // observeOn(AndroidSchedulers.mainThread()) 때문에 이 메서드는 항상 메인 스레드에서 실행됩니다.
                        System.out.println("RxJava 데이터 수신 - Thread: " + Thread.currentThread().getName());

                        // UI 스레드에서 TextView의 텍스트를 업데이트합니다.
                        textViewResult.setText(s);
                    }

                    @Override
                    public void onError(Throwable e) {
                        // Observable이 onError(exception)를 호출하여 오류를 발행했을 때 호출됩니다.
                        // 이 메서드도 메인 스레드에서 실행됩니다.
                        System.err.println("RxJava 오류 발생 - Thread: " + Thread.currentThread().getName() + ": " + e.getMessage());

                        // 오류 상태를 UI에 표시하는 메서드를 호출합니다.
                        handleErrorState("데이터 로딩 실패: " + e.getMessage());
                    }

                    @Override
                    public void onComplete() {
                        // Observable이 onComplete()를 호출하여 작업이 성공적으로 완료되었음을 알릴 때 호출됩니다.
                        // 이 메서드도 메인 스레드에서 실행됩니다.
                        System.out.println("RxJava 작업 완료됨 - Thread: " + Thread.currentThread().getName());

                        // 모든 작업이 완료되었음을 알리고 UI를 완료 상태로 변경하는 메서드를 호출합니다.
                        showCompletedState();
                    }
                });
    }

    /**
     * 비동기 작업이 진행 중일 때 UI를 업데이트하는 메서드입니다.
     * ProgressBar를 보여주고, 결과 TextView는 숨기며, 버튼을 비활성화합니다.
     */
    private void showLoadingState() {
        Log.d(TAG, "showLoadingState: 프로그레스바를 활성화합니다.");
        progressBar.setVisibility(View.VISIBLE); // ProgressBar를 보이게 합니다.
        textViewResult.setVisibility(View.GONE);  // 결과 TextView는 숨깁니다.
        buttonLoadData.setEnabled(false);         // 버튼을 비활성화하여 중복 클릭을 방지합니다.
    }

    /**
     * 비동기 작업이 성공적으로 완료되었을 때 UI를 업데이트하는 메서드입니다.
     * ProgressBar를 숨기고, 결과 TextView를 다시 보여주며, 버튼을 활성화합니다.
     */
    private void showCompletedState() {
        Log.d(TAG, "showCompletedState: 작업이 완료되어 프로그레스바를 숨깁니다.");
        progressBar.setVisibility(View.GONE);   // ProgressBar를 숨깁니다.
        textViewResult.setVisibility(View.VISIBLE); // 결과 TextView를 다시 보이게 합니다.
        buttonLoadData.setEnabled(true);          // 버튼을 다시 활성화합니다.
    }

    /**
     * 비동기 작업 중 오류가 발생했을 때 UI를 업데이트하는 메서드입니다.
     * ProgressBar를 숨기고, 오류 메시지를 TextView에 표시하며, Toast 메시지를 보여주고, 버튼을 활성화합니다.
     * @param errorMessage 표시할 오류 메시지
     */
    private void handleErrorState(String errorMessage) {
        progressBar.setVisibility(View.GONE);   // ProgressBar를 숨깁니다.
        textViewResult.setVisibility(View.VISIBLE); // 결과 TextView를 보이게 합니다.
        textViewResult.setText(errorMessage);   // TextView에 오류 메시지를 설정합니다.
        buttonLoadData.setEnabled(true);          // 버튼을 다시 활성화합니다.
        Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show(); // 사용자에게 오류를 알립니다.
    }

    /**
     * Activity의 생명주기 메서드 중 하나로, Activity가 소멸될 때 호출됩니다.
     * 여기서 RxJava 구독을 해제하는 것이 매우 중요합니다.
     * 그렇지 않으면 액티비티가 파괴된 후에도 백그라운드 작업이 계속 실행되어 메모리 누수가 발생할 수 있습니다.
     */
    @Override
    protected void onDestroy() {
        super.onDestroy(); // 반드시 super 메서드를 먼저 호출해야 합니다.

        // CompositeDisposable이 null이 아니고 아직 해제되지 않았다면,
        // 모든 구독을 해제합니다.
        if (disposables != null && !disposables.isDisposed()) {
            disposables.dispose(); // 모든 구독 취소
            System.out.println("RxJava 구독 해제됨");
        }
    }
}

 

시연영상