주요글: 도커 시작하기
반응형

안드로이드에서 비동기로 작업을 할 때 사용되는 AsyncTask는 잘 사용하지 않으면, 코드 유지보수를 어렵게 만드는 악의 원천이 될 수 있다. 특히, 많은 안드로이드 입문서들이  AsyncTask를 이용해서 비동기를 처리하는 코드를 보여줄 때 다음의 두 가지 문제점을 포함하고 있는 경우가 많다.

  • AsyncTask를 상속 받은 클래스가 비동기 처리 결과를 사용하는 코드에 대한 의존을 갖는다.
  • 비동기 처리가 필요한 기능마다 AsyncTask 클래스를 상속받아 구현한다.
우선, 첫 번째 문제는 코드의 응집도를 낮추고 결합도를 높여주기 때문에, 코드가 만들어지면 만들어질수록 유지보수를 하기 어렵게 만들어준다. 다음은 전형적인 코드이다.

public class AsyncHttpTask extends AsyncTask<String, Void, String> {
private Handler handler;
private Exception exception;

public AsyncHttpTask(Handler handler) {
this.handler = handler;
}

@Override
protected T doInBackground(String... urls) {
try {
// urls[0]의 URL부터 데이터를 읽어와 String으로 리턴
...
return responseData;
} catch(Exception ex) {
this.exception = ex;
return null;
}
}

@Override
protected void onPostExecute(String responseData) {
if (exception != null) {
Message msg = handler.obtainMessage();
msg.what = -1;
msg.obj = exception;
handler.sendMessage(msg);
return;
} else {
Message msg = handler.obtainMessage();
msg.what = 0;
msg.obj = responseData;
handler.sendMessage(msg);
}
}
}


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 객체를 참조해야 할 경우에 사용되는 인터페이스이다.
AsyncCallback 인터페이스

AsyncCallback 인터페이스는 다음과 같이 정의하였다.

public interface AsyncCallback<T> {
    public void onResult(T result);

    public void exceptionOccured(Exception e);

    public void cancelled();
}

각 메서드는 결과를 받고(onResult), 처리 도중 발생한 익셉션을 받고(exceptionOccured), 작업을 취소했음을 받을(cancelled) 때 사용된다. 필요에 따라 진행율을 받을 수 있는 콜백 메서드를 추가할 수도 있을 것이다.

AsyncExecutorAware 인터페이스

콜백 구현 객체에서 AsyncExecutor에 대한 접근이 필요할 때가 있는데, 이런 경우 콜백 구현 클래스는 AsyncExecutorAware 인터페이스를 구현하면 된다.

public interface AsyncExecutorAware<T> {

public void setAsyncExecutor(AsyncExecutor<T> asyncExecutor);

}

뒤에서 구현할 AsyncExecutor는 콜백 객체가 AsyncExecutorAware 인터페이스를 구현한 경우, 콜백 객체의 setAsyncExecutor()를 호출하여 자기 자신을 콜백 객체에 전달한다.

AsyncExecutor 클래스

AsyncExecutor는 다음과 같이 구현하였다.

import java.util.concurrent.Callable;

import android.os.AsyncTask;
import android.util.Log;

public class AsyncExecutor<T> extends AsyncTask<Void, Void, T> {
private static final String TAG = "AsyncExecutor";

private AsyncCallback<T> callback;
private Callable<T> callable;
private Exception occuredException;

public AsyncExecutor<T> setCallable(Callable<T> callable) {
this.callable = callable;
return this;
}

public AsyncExecutor<T> setCallback(AsyncCallback<T> callback) {
this.callback = callback;
processAsyncExecutorAware(callback);
return this;
}

@SuppressWarnings("unchecked")
private void processAsyncExecutorAware(AsyncCallback<T> callback) {
if (callback instanceof AsyncExecutorAware) {
((AsyncExecutorAware<T>) callback).setAsyncExecutor(this);
}
}

@Override
protected T doInBackground(Void... params) {
try {
return callable.call();
} catch (Exception ex) {
Log.e(TAG,
"exception occured while doing in background: "
+ ex.getMessage(), ex);
this.occuredException = ex;
return null;
}
}

@Override
protected void onPostExecute(T result) {
if (isCancelled()) {
notifyCanceled();
}
if (isExceptionOccured()) {
notifyException();
return;
}
notifyResult(result);
}

private void notifyCanceled() {
if (callback != null)
callback.cancelled();
}

private boolean isExceptionOccured() {
return occuredException != null;
}

private void notifyException() {
if (callback != null)
callback.exceptionOccured(occuredException);
}

private void notifyResult(T result) {
if (callback != null)
callback.onResult(result);
}

}

AsyncExecutor의 주요 기능은 다음과 같다.
  • doInBackground() 메서드: callable 객체에 작업 실행을 위임한다.
  • onPostExecute() 및 관련 메서드: callback 객체에 결과를 전달한다.
사용 예시

AsyncExecutor의 사용 방법은 다음과 같다.

public void load() {
// 비동기로 실행될 코드
Callable<ToonDataList> callable = new Callable<ToonDataList>() {
@Override
public ToonDataList call() throws Exception {
return client.getFeaturedTopData(startRow);
}
};

new AsyncExecutor<ToonDataList>()
.setCallable(callable)
.setCallback(callback)
.execute();
}

// 비동기로 실행된 결과를 받아 처리하는 코드
private AsyncCallback<ToonDataList> callback = new AsyncCallback<ToonDataList>() {
@Override
public void onResult(ToonDataList result) {
appendResult(result);
}

@Override
public void exceptionOccured(Exception e) {
AlertUtil.alert(context, context.getString(R.string.dataloading_error));
}

@Override
public void cancelled() {
}
};


기타

AsyncCallback 인터페이스의 두 메서드 중 한 개만 구현하면 되는 클래스를 위해 아무것도 하지 않는 구현을 제공한 Base 클래스를 추가했다.

public interface AsyncCallback<T> {
...
public static abstract class Base<T> implements AsyncCallback<T> {

@Override
public void exceptionOccured(Exception e) {
}

@Override
public void cancelled() {
}

}
}

관련자료


+ Recent posts