안드로이드에서 비동기로 작업을 할 때 사용되는 AsyncTask는 잘 사용하지 않으면, 코드 유지보수를 어렵게 만드는 악의 원천이 될 수 있다. 특히, 많은 안드로이드 입문서들이 AsyncTask를 이용해서 비동기를 처리하는 코드를 보여줄 때 다음의 두 가지 문제점을 포함하고 있는 경우가 많다.
- AsyncTask를 상속 받은 클래스가 비동기 처리 결과를 사용하는 코드에 대한 의존을 갖는다.
- 비동기 처리가 필요한 기능마다 AsyncTask 클래스를 상속받아 구현한다.
AsyncHttpTask를 필요로 하는 코드는 Handler 객체를 이용해서 결과를 받아온다.
public class SomeActivity extends Activity {
public void some() {
new AsyncHttpTask(handler).execute(url);
}
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch(msg.what) {
case -1:
// 에러 처리
break;
case 0:
// 정상 응답 처리
break;
}
}
};
}
AsyncHttpTask를 사용하는 모든 코드는 위와 같이 결과를 받아오기 위해 Handler를 사용해야 하고, -1과 0 이라는 숫자를 사용해야 한다. (-1과 0 대신에 ASYNC_HTTP_ERROR, ASYNC_HTTP_OK 라는 상수를 사용하면 조금은 나을 것 같다.) 문제는 Handler를 이곳 저곳에서 사용하기 시작하면, case 문이 점점 복잡해질 수 있다는 것이다. 예를 들어, 또 다른 Async 처리 기능이 있다고 해 보자. 이 경우 위 코드는 다음과 같이 될 것이다.
public class SomeActivity extends Activity {
public void some() {
new AsyncHttpTask(handler).execute(url);
}
public void process() {
new AsyncImageFileProcessingTask(handler).execute(file);
}
private Handler handler = new Handler() {
public void handleMessage(Message msg) {
switch(msg.what) {
case -1:
// 에러 처리
break;
case 0:
// 정상 응답 처리
break;
case 101:
// 파일 프로세싱 시작
break;
case 102:
// 파일 프로세싱 종료
break;
}
}
};
}
여기서 짜증나는 건 AsyncHttpTask와 AsyncImageFileProcessingTask가 동일한 Handler를 공유할 수 있기 때문에, 서로 Message의 what 값이 충돌나지 않게 구현해야 한다는 점이다. AsyncImageFileProcessingTask가 실패할 경우 what 값을 -1로 준다면, 위 코드는 문제가 발생한다. 즉, 서로 다른 비동기 처리 코드임에도 불구하고 Handler를 사용함으로써 암묵적인 커플링이 발생하는 것이다.
그래서, Handler를 사용해서 결과를 주고 받는 방식보다는 콜백 인터페이스를 이용해서 결과를 받는 것이 코드 유지보수에 유리하다. (이에 대한 내용은 예전에 쓴 '안드로이드 Handler 사용으로 인해 흩어진 코드 커맨드와 콜백으로 정리하기' 글을 참고한다.)
유지보수를 고려한 비동기 처리 기반 코드
최근에 안드로이드 코딩 중, 다른 종류의 비동기 작업을 해야할 일이 생겨서, 위에서 언급한 문제를 최대한 제거한 비동기 처리 기반 코드를 만들어보았다. 이 코드에서 출현하는 타입은 세 개이다.
- AsyncExecutor<T> - AsyncTask를 상속받은 클래스로서, 비동기로 작업을 실행하고 결과를 전달해주는 기능이다.
- Callable<T> - java.util.concurrent.Callable 인터페이스로 비동기로 실행할 작업을 제공한다.
- AsyncCallback<T> - 비동기 처리 결과를 받을 때 사용되는 콜백 인터페이스이다.
- AsyncExecutorAware<T> - 콜백 구현체가 AsyncExecutor 객체를 참조해야 할 경우에 사용되는 인터페이스이다.
- doInBackground() 메서드: callable 객체에 작업 실행을 위임한다.
- onPostExecute() 및 관련 메서드: callback 객체에 결과를 전달한다.
- 안드로이드 Handler 사용으로 인해 흩어진 코드 커맨드와 콜백으로 정리하기
http://javacan.tistory.com/entry/increase-cohesion-using-command-and-callback-insteadof-handler-in-android
-
-
대관령 2014.04.08 22:19
감동의 연속입니다.
제가 몇일동안 고민하던 사항들을 한방에 날려주시는군요.
한편으로는 새로운 자극을 받게되어 감사드립니다.
화이팅입니다~~ -
비동기 2014.07.03 13:43
좋은 글 감사합니다^^
궁금한게 있는데요.
한 화면에서 여러가지 비동기 처리를 할 때는 작업의 구분을 어떻게 해야 할까요?
AsyncCallback 함수의 onResult 에서 처리를 하려면 Handler에서 처리하는 것 처럼 상수 값이나 다른 구분값이 필요할 것 같은데요...
아니면 AsyncCallback 를 비동기 처리 갯수만큼 만들어야 하나요?
제가 제대로 이해를 못한건지.. 답변 부탁드릴게요 ㅠ
-
오리왕 2014.07.06 09:14
callable 내에서 진행률을 처리해야 할 경우, AsyncTask 의 progress 즉 진행률을 비동기로 받으려면 어떻게 해야 하나요?
제가 생각한 구조는 아래의 코드와 같습니다. 이렇게 된다면 callable 의 call() 내부에서 callback 을 호출하는 부분(callback.onProgress) 이 필요한데요...
이 부분의 구현에 대한 힌트 또는 예제 코드를 얻을수 있을까요?
public interface AsyncCallback<T> {
public void onResult(T result);
public void onProgress(Integer progress); // 진행률 표시와 관련된 부분
public void exceptionOccured(Exception e);
public void cancelled();
}
doInBackground(Void... params) {
try {
// 진행률 표시부 시작
int progress = 10;
if (callback != null)
callback.onProgress(progress);
...
// 진행률 표시부 종료
return callable.call();
} catch (Exception e) {
}
}-
최범균 madvirus 2014.07.08 10:12 신고
말씀하신 것처럼 AsyncCallback 에는 onProgress 메서드를 추가해야 할 것 같고, 진행률을 누가 전달할지가 관건일 듯 합니다.
실제 진행률을 알고 있는 건, callable 이지 AsyncExecutor는 아니니까요. AsyncExecutor가 AsyncTask를 상속받았으니까, 결과적으로
- callable에서 AsyncExecutor 객체의 publishProgress()를 실행하고,
- AsyncExecutor 객체의 onProgressUpdate()에서 callback의 onProgress()를 호출하도록 하면 될 것 같습니다.
-
-
beetcom 2014.08.01 02:14
callable에서 AsyncExecutor aware 하더라도 publishProgress는 protected 니까
callable이 publishProgress를 직접 호출할 수는 없을 것 같은데요.
뭔가 깔끔한 방법은 안떠오르네요.-
최범균 madvirus 2014.08.04 19:06 신고
글을 쓴지 좀 되서 기억을 복구하는데 꽤 시간이 걸리네요 ^^; (한해 한해 기억력이 뚝뚝 떨어짐을 느낍니다.)
조금 더 나가보자면 다음과 같이 해 볼 수 있을 것 같습니다.
* AsyncExecutor를 인터페이스로 만들고,
* AsyncExecutor 인터페이스에 notifyProgress()와 같은 메서드를 추가하고,
* AsyncExecutor를 구현한 AsyncTaskExecutor 클래스가 AsyncTask 클래스를 상속받도록 한 다음에
* AsyncTaskExecutor 클래스의 notifyProgress() 메서드가 상위 클래스의 publishProgress()를 호출하게 한다거나 하는 식으로
해 보면 조금 쓸만한 모양이 나오지 않을까 싶습니다. 해보지 않고 상상으로만 적어본거라 어떨지 모르겠네요.
-
-
sinki 2015.01.08 17:12
우선 정말 좋은 예제 감사드립니다.
상기 예제에서 정말 이해가 안가는 부분이 있는데
AsyncExecutorAware 인터페이스 에서
"콜백 구현 객체에서 AsyncExecutor에 대한 접근이 필요할 때가 있는데"
실제 어떻게 구현 해야 하는지 감 조차 안 옵니다. ^^
많이 바쁘시겠지만, 힌트라도 주시겠어요?
미리 감사합니다.
-
최범균 madvirus 2015.01.12 12:25 신고
클래스가 두 개 인터페이스를 상속받도록 구현하시면 됩니다.
public class MyCallback implements AsyncExecutorAware, AsyncCallback<Some>() {
private AsyncExecutor<Some> executor;
public void setAsyncExecutor(AsyncExecutor<Some> executor) {
this.executor = executor;
}
// 다른 메서드들 구현
}
-