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>