안드로이드 (Deprecated) AsyncTask 대체하기
안드로이드에서 백그라운드 작업을 하기 위해 자주 사용하는 AsyncTask가 사망선고를 받았다.
작년 하반기에 올라온 이 커밋에서 AsyncTask에 @Deprecated가 붙었고, 커멘트에 다음과 같이 나와 있다.
AsyncTask was intended to enable proper and easy use of the UI thread. However, the most common use case was for integrating into UI, and that would cause Context leaks, missed callbacks, or crashes on configuration changes. It also has inconsistent behavior on different versions of the platform, swallows exceptions from doInBackground, and does not provide much utility over using Executors directly.
없애는 이유를 장황하게 늘어놓았는데, 사실 이건 일반적인 스레드 프로그래밍이 가지고 있는 위험성과 크게 다르지 않다. Thread 자체를 없앨 수도 없는데, 괜히 편하게 쓸 수 있는 Class를 날려버리는게 이해가 안간다. 몇 십 년이 지나도록 Windows API가 호환성을 죽어라 지켜나가는 것과 비교하면, 이게 Google의 스타일이기도 하지만, 결국 아키텍트의 역량 차이라는 생각이 든다. 자주 안드로이드 앱을 만든건 아니지만 나 역시도 안드로이드 초창기부터 즐겨 사용하던 AsyncTask라서 상당히 아쉽다.
아무튼 그래서 대체재를 찾아보았는데, 여러 라이브러리가 많았지만, 대부분 RxJava를 추천했다. 그런데 기본기능 구현을 특정 라이브러리에 종속시키기도 좀 그랬고, 또 RxJava 스타일이 개인 취향에 안 맞았다. 자바가 스크립트 언어도 아니고, 반 네이티브의 위치에 올라 있고, 나름 자바 스타일이 있는데... 그래서 그냥 AsyncTask의 열화 버전을 만들기로 했다.
클래스의 이름은 generic하게 ThreadTask로 했다. 형식은 AsyncTask를 충실히 따라서 onPreExecute, doInBackground, onPostExecute를 제공하기로 했다. ThreadTask는 Abstract Class로 위 세 개의 메서드를 오버라이드 해야 한다. 간단한 구현이므로 코드만 봐도 직관적으로 이해가 될 것이다.
public abstract class ThreadTask<T1,T2> implements Runnable {
// Argument
T1 mArgument;
// Result
T2 mResult;
// Handle the result
public final int WORK_DONE = 0;
Handler mResultHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
// Call onPostExecute
onPostExecute(mResult);
}
};
// Execute
final public void execute(final T1 arg) {
// Store the argument
mArgument = arg;
// Call onPreExecute
onPreExecute();
// Begin thread work
Thread thread = new Thread(this);
thread.start();
}
@Override
public void run() {
// Call doInBackground
mResult = doInBackground(mArgument);
// Notify main thread that the work is done
mResultHandler.sendEmptyMessage(WORK_DONE);
}
// onPreExecute
protected abstract void onPreExecute();
// doInBackground
protected abstract T2 doInBackground(T1 arg);
// onPostExecute
protected abstract void onPostExecute(T2 result);
}
이 클래스는 AsyncTask와 유사하게 이용할 수 있다.
다음은 ThreadTask를 이용해서 2^1 ~ 2^4의 합을 구하는 예제이다.
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final TextView tv = (TextView) findViewById(R.id.TV_RESULT);
new ThreadTask<Integer, Integer>() {
@Override
protected void onPreExecute() {
}
@Override
protected Integer doInBackground(Integer arg) {
int result = 0, index = 0;
while (index++ < arg) {
result += (1<<index);
}
return result;
}
@Override
protected void onPostExecute(Integer result) {
tv.setText(result.toString());
}
}.execute(4);
}
}
onPreExecute()와 onPostExecute()는 불렀던 스레드에서 실행이 되는데, 주로 UI 스레드에서 부를 것이기에 UI 작업이 가능하다.
doInBackground는 execute()에 의해서 생성되는 워커 스레드 안에서 작동된다.
개인적으로는 필요가 없어 구현을 안했는데, 다음 두 가지 이슈가 나올 수 있다.
먼저, doInBackground() 내에서 UI 작업이 불가능하다.
이 부분은 Activity.runOnUIThread()라는 메서드가 제공되고 있으므로 이를 활용할 수 있다.
여기를 참조하면 된다.
다음으로 여러 ThreadTask에 대한 동기화가 고려되어 있지 않다.
여러 개의 ThreadTask가 돌아가는 상황에서 복수의 스레드가 공유자원에 접근하면 동기화가 안된다.
이것은 자원에 접근하는 부분에 syncronized 등 안전 장치를 할 필요가 있다.
[수정 사항 2021. 1]
글 작성시에 올렸던 ThreadTask의 코드가 비동기 처리가 되어 있지 않아 수정함.
- Thread를 join하는 부분 삭제
- Main thread의 handler에서 onPostExecute() 호출
의식하지 못하고 있었는데 댓글로 알려주신 분들이 있어서 감사합니다!
[수정 사항 2024. 3]
Handler의 기본 생성자가 deprecated되어서 수정함.
- Handler 생성자에 Looper.getMainLooper()로 Looper를 만들어 전달
Fin.