저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

'AsyncExecutorAware'에 해당되는 글 1건

  1. 2013.01.30 유지보수를 고려한 안드로이드 비동기 처리 기반 코드 만들기 (14)

안드로이드에서 비동기로 작업을 할 때 사용되는 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() {
}

}
}

관련자료


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 뉴비뉴비 2013.05.14 11:29 신고  댓글주소  수정/삭제  댓글쓰기

    아 너무 좋네요 코드가 완전 깔끔해졌어요~ 잘보고갑니다 감사합니다 ^ 0^

  2. 식빵 2013.07.23 16:39 신고  댓글주소  수정/삭제  댓글쓰기

    정말 신세계를 보고 가네요..=ㅂ=!!!

  3. 대관령 2014.04.08 22:19 신고  댓글주소  수정/삭제  댓글쓰기

    감동의 연속입니다.
    제가 몇일동안 고민하던 사항들을 한방에 날려주시는군요.
    한편으로는 새로운 자극을 받게되어 감사드립니다.
    화이팅입니다~~

  4. 비동기 2014.07.03 13:43 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 글 감사합니다^^

    궁금한게 있는데요.

    한 화면에서 여러가지 비동기 처리를 할 때는 작업의 구분을 어떻게 해야 할까요?

    AsyncCallback 함수의 onResult 에서 처리를 하려면 Handler에서 처리하는 것 처럼 상수 값이나 다른 구분값이 필요할 것 같은데요...

    아니면 AsyncCallback 를 비동기 처리 갯수만큼 만들어야 하나요?

    제가 제대로 이해를 못한건지.. 답변 부탁드릴게요 ㅠ

  5. 오리왕 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()를 호출하도록 하면 될 것 같습니다.

  6. 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()를 호출하게 한다거나 하는 식으로
      해 보면 조금 쓸만한 모양이 나오지 않을까 싶습니다. 해보지 않고 상상으로만 적어본거라 어떨지 모르겠네요.

  7. 김형주 2014.09.04 12:01 신고  댓글주소  수정/삭제  댓글쓰기

    자료 감사합니다.

    설명해주신 자료를 보면 AsynCallback으로 try/catch 의 Exception 관련 에러메시지는 받을 수 있지만,
    HttpResponse Status Code (ex: 404, 500 .. ) 에 관련된 에러 코드는 받을 수 없는것 같습니다.

    원하는 코드까지 받으려면 AsyncHttpTask 에서 리턴되는 결과값을 HashMap으로 대체해서
    응답값과 코드까지 받으려고 하는데.. 문제가 될까요?


  8. 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;
      }
      // 다른 메서드들 구현

      }