Android Handler 메커니즘 완벽 이해하기

Android에서 Handler는 서로 다른 스레드 간에 메시지를 전달하거나 작업을 실행하는 데 사용되는 핵심 도구이며, 특히 백그라운드 스레드에서 메인 스레드(UI 스레드)로 작업을 전송합니다. Handler의 작동 원리는 Looper와 MessageQueue에 의존하며, 이 세 가지가 Android 메시지 처리 메커니즘을 구성합니다.

핵심 구성 요소

스레드와 메시지 큐: 각 Android 앱의 메인 스레드에는 지속적으로 실행되며 메시지 처리를 기다리는 Looper가 있습니다. 메시지는 MessageQueue라는 큐에 저장되며, Handler는 이 큐에 Message 또는 Runnable을 보내고 해당 스레드에 분배하는 역할을 합니다.

Looper: Looper는 메시지 루프의 핵심입니다. 각 스레드는 최대 하나의 Looper를 가질 수 있으며, Looper.loop()을 호출하여 메시지 큐의 작업을 처리하는 무한 루프를 시작합니다. 메인 스레드는 기본적으로 Looper가 있지만, 작업자 스레드는 수동으로 생성해야 합니다.

MessageQueue: 각 스레드의 Looper는 MessageQueue를 가지고 있습니다. 이 큐는 처리 대기 중인 Message 또는 Runnable을 저장하며, 시간 순서대로 정렬되어 작업을 순차적으로 큐에 넣습니다.

Handler: Handler는 특정 스레드의 Looper와 연결된 도구로, Message 또는 Runnable을 MessageQueue에 넣습니다. 또한 메시지 큐에서 메시지를 꺼내 해당 스레드에서 실행합니다.

작동 흐름

1. Looper 초기화: 메인 스레드가 시작될 때 자신의 Looper를 만들고 Looper.loop()을 호출하여 메시지 루프를 시작합니다.

Looper.prepare(); // 현재 스레드에 Looper 준비
Looper.loop();   // 메시지 루프 시작

메인 스레드의 경우 이 과정이 Android 시스템에서 자동으로 수행되지만, 작업자 스레드에서는 수동으로 Looper.prepare()와 Looper.loop()을 호출해야 합니다.

2. MessageQueue: Looper는 FIFO(선입선출) 방식의 MessageQueue를 가지고 있습니다. MessageQueue는 Handler를 통해 전송된 모든 Message 또는 Runnable을 저장합니다.

3. Handler 메시지 전송: 특정 스레드에서 Handler를 만들면 기본적으로 해당 스레드의 Looper와 바인딩됩니다. Handler.post()Handler.sendMessage()로 MessageQueue에 작업이나 메시지를 추가할 수 있습니다.

4. Looper 메시지 처리: Looper.loop()이 무한 루프에 진입하여 MessageQueue에 처리할 작업이 있는지 확인합니다. 있으면 메시지를 꺼내 해당 Handler의 handleMessage() 메서드로 전달합니다.

5. Handler 메시지 처리: Looper가 메시지를 꺼내면 Handler의 dispatchMessage()를 호출하여 실행합니다. dispatchMessage()는 메시지가 Runnable인지 Message인지 확인하고 적절히 처리합니다. Message인 경우 handleMessage()를 호출합니다.

흐름 다이어그램

  • 앱이 시작되면 메인 스레드가 Looper와 MessageQueue를 만듭니다. Handler가 생성되어 Looper와 바인딩되고, MessageQueue에 메시지를 넣습니다.
  • 백그라운드 스레드나 다른 구성 요소가 Handler를 통해 메인 스레드의 MessageQueue로 작업을 전송합니다. post()로 Runnable을 보내거나 sendMessage()로 Message를 보냅니다.
  • 메인 스레드의 Looper가 메시지 루프에 진입하여 MessageQueue에서 메시지를 꺼내 Handler에 전달하고, handleMessage()나 Runnable이 작업을 실행합니다.

주요 구성 요소 상세

Looper: Android 메시지 메커니즘의 핵심으로, 메시지 큐를 폴링합니다. 각 스레드는 Looper.prepare()로 Looper를 만들고 Looper.loop()으로 메시지 루프를 시작할 수 있습니다.

MessageQueue: 스레드의 메시지 큐로, 실행할 작업을 저장합니다. FIFO 원칙으로 메시지를 처리합니다.

Message: 스레드 간 데이터를 전달하는 객체로, 데이터와 명령어를 포함할 수 있습니다.

Runnable: 코드를 실행하기 위한 인터페이스입니다. Handler.post()를 통해 MessageQueue에 제출되어 특정 스레드에서 실행됩니다.

메인 스레드의 Looper 메커니즘: Android 앱이 시작될 때 메인 스레드는 자신의 Looper를 만들고 Looper.loop()을 호출하여 시스템이나 앱이 보낸 메시지(예: UI 업데이트)를 처리할 수 있습니다.

정리하면, Android의 Handler, Looper, MessageQueue는 스레드 간 통신의 기본 메커니즘을 구성합니다. Handler는 MessageQueue에 작업을 제출하고, Looper는 큐에서 작업을 꺼내 실행합니다. 이 메커니즘을 통해 개발자는 백그라운드 스레드에서 시간이 오래 걸리는 작업을 수행하고 결과를 메인 스레드로 전달하여 UI 블로킹을 방지할 수 있습니다.

실습 예제 1: 기본 Handler 사용

MainActivity.java

package com.example.handler;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    private TextView textView;
    private Button btnUpdate;

    Handler handler = new Handler(Looper.getMainLooper()) {
        @Override
        public void handleMessage(@NonNull Message msg) {
            if (msg.what == 1) {
                textView.setText("메시지 수신: 1");
            } else if (msg.what == 2) {
                textView.setText("메시지 수신: 2");
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = findViewById(R.id.textView);
        btnUpdate = findViewById(R.id.btnUpdate);
        btnUpdate.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        Message message = handler.obtainMessage();
                        message.what = 2;
                        handler.sendMessage(message);
                    }
                }).start();
            }
        });
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="100dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="200dp"
        android:text="메시지 수신: 1" />
    <Button
        android:id="@+id/btnUpdate"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="100dp"
        android:text="Handler로 TextView 업데이트" />
</LinearLayout>

실행 시 처음에는 "메시지 수신: 1"이 표시되고, 버튼 클릭 후 "메시지 수신: 2"로 변경됩니다.

실습 예제 2: ListView와 SQLite

MainActivity.java

package com.example.handler;

import android.database.Cursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import androidx.appcompat.app.AppCompatActivity;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    ListView userListView;
    private ArrayAdapter<String> adapter;
    private List<String> userData;
    private Handler handler;
    private DatabaseHelper dbHelper;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        userListView = findViewById(R.id.userListView);
        userData = new ArrayList<>();
        adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, userData);
        userListView.setAdapter(adapter);
        dbHelper = new DatabaseHelper(this);
        handler = new Handler(Looper.getMainLooper());

        new Thread(new Runnable() {
            @Override
            public void run() {
                dbHelper.insertUser("rose", 23);
                dbHelper.insertUser("Jordan", 62);
                dbHelper.insertUser("김철수", 32);
                dbHelper.insertUser("이영희", 43);
                dbHelper.insertUser("황노야", 55);

                Cursor cursor = dbHelper.getAllUsers();
                final List<String> users = new ArrayList<>();
                if (cursor != null && cursor.moveToFirst()) {
                    do {
                        String name = cursor.getString(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_NAME));
                        int age = cursor.getInt(cursor.getColumnIndexOrThrow(DatabaseHelper.COLUMN_AGE));
                        users.add("이름: " + name + ", 나이: " + age);
                    } while (cursor.moveToNext());
                    cursor.close();
                }

                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        userData.clear();
                        userData.addAll(users);
                        adapter.notifyDataSetChanged();
                    }
                });
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacksAndMessages(null); // 메모리 누수 방지
    }
}

DatabaseHelper.java

package com.example.handler;

import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.Nullable;

public class DatabaseHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "school.db";
    private static final int DATABASE_VERSION = 1;
    public static final String TABLE_USERS = "users";
    public static final String COLUMN_ID = "id";
    public static final String COLUMN_NAME = "name";
    public static final String COLUMN_AGE = "age";

    public DatabaseHelper(@Nullable Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    private static final String CREATE_TABLE_USERS = "CREATE TABLE " +
            TABLE_USERS + "("
            + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
            + COLUMN_NAME + " TEXT, "
            + COLUMN_AGE + " INTEGER)";

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE_USERS);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_USERS);
        onCreate(db);
    }

    public void insertUser(String name, int age) {
        SQLiteDatabase db = this.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(COLUMN_NAME, name);
        values.put(COLUMN_AGE, age);
        db.insert(TABLE_USERS, null, values);
        db.close();
    }

    public Cursor getAllUsers() {
        SQLiteDatabase db = this.getReadableDatabase();
        return db.query(TABLE_USERS, null, null, null, null, null, null);
    }
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/userListView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</androidx.constraintlayout.widget.ConstraintLayout>

실습 예제 3: ListView와 Room

MainActivity.java

package com.example.handler;

import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.room.Room;
import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private ListView listView;
    private AppDatabase db;
    private ArrayAdapter<String> adapter;
    private List<String> userData;
    private Handler handler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "app.db").build();
        listView = findViewById(R.id.userListView);
        userData = new ArrayList<>();
        adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, userData);
        listView.setAdapter(adapter);
        handler = new Handler(Looper.getMainLooper());

        new Thread(new Runnable() {
            @Override
            public void run() {
                db.userDao().insert(new User("rose", 21));
                db.userDao().insert(new User("황용", 18));
                db.userDao().insert(new User("곽정", 21));
                db.userDao().insert(new User("목염자", 19));

                List<User> users = db.userDao().getAll();
                final List<String> userNames = new ArrayList<>();
                for (User u : users) {
                    userNames.add("이름: " + u.getUserName() + ", 나이: " + u.getAge());
                }

                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        userData.clear();
                        userData.addAll(userNames);
                        adapter.notifyDataSetChanged();
                    }
                });
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacksAndMessages(null); // 메모리 누수 방지
    }
}

AppDatabase.java

package com.example.handler;

import androidx.room.Database;
import androidx.room.RoomDatabase;

@Database(entities = {User.class}, version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}

User.java

package com.example.handler;

import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;

@Entity(tableName = "users")
public class User {
    @PrimaryKey(autoGenerate = true)
    private int id;
    @ColumnInfo(name = "user_name")
    private String userName;
    private int age;

    public User(String userName, int age) {
        this.userName = userName;
        this.age = age;
    }

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getUserName() { return userName; }
    public void setUserName(String userName) { this.userName = userName; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
}

UserDao.java

package com.example.handler;

import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;

@Dao
public interface UserDao {
    @Insert
    void insert(User user);
    @Query("SELECT * FROM users")
    List<User> getAll();
}

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/userListView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</androidx.constraintlayout.widget.ConstraintLayout>

태그: Android Handler Looper MessageQueue 스레드

7월 7일 19:05에 게시됨