안드로이드 AsyncTask. (Android AsyncTask)

2020. 8. 3. 00:28


1. 비동기 작업을 위한 AsyncTask.

[안드로이드 프로그래밍. 스레드.]의 여러 글들을 통해, 안드로이드에서 스레드를 사용해야 하는 이유와 새로운 스레드를 실행하는 방법, 그리고 핸들러를 통한 스레드 간 통신 방법에 대해 알아보았습니다. 스레드에 대한 기초적인 내용부터, ThreadHandler, Message, Runnable에 대한 기본 개념과 사용법 등을 설명했었는데요, 이제, 안드로이드 스레드에 대한 기초 지식을 넘어, 앱 개발에 조금 더 실용적으로 적용할 수 있는 내용을 다뤄볼까 합니다.


바로, 비동기(Asynchronous) 작업을 위해 사용하는 AsyncTask에 대한 내용입니다.


2. 스레드 사용의 몇 가지 유형.

안드로이드에서 스레드를 사용하는 이유. 기억하시나요?


[안드로이드 스레드 예제. 스레드로 고민해보기. (Android Thread UseCase)]에서 예제를 통해 알아보았듯이, 안드로이드의 메인 스레드는 앱 실행에 있어 가장 중요한 역할, 즉, 화면 그리기 및 이벤트 처리 등의 역할을 수행하기 때문에, 절대로 실행이 멈추거나 일정 시간 이상 지연되어서는 안됩니다. 그래서 메인 스레드의 실행에 영향을 줄 수 있는 코드는 별도의 스레드에서 실행해야 한다고 설명했었죠.


더불어 [ANDROID 프로그래밍. THREAD. ] 카테고리에, 스레드에 대한 기본 개념과 사용법, 스레드 통신에 대한 내용을 다루었습니다.


그런데 실제 프로그램 개발에서 스레드를 만들고 실행할 때, 스레드의 생성 시점과 종료 시점, 그리고 메인 스레드와의 메시지 통신 여부를 따져보면, 스레드 동작 형태를 아래 그림과 같이 몇 가지 유형으로 나눌 수 있습니다.


스레드 동작 형태 유형


위의 그림에서 보여지는 유형들이 객관적 이론에 따라 정립된 개념을 표현한 것은 아닙니다. 개인의 경험에 기반하여 주관적 기준으로 구분된 형태를, 보기 쉬운 그림 형태로 표현한 것일 뿐입니다. 그러므로 동작 유형의 종류와 개념에 너무 큰 의미를 부여할 필요는 없습니다. 그냥, "스레드를 이러한 형태로 사용할 수 있다"라는 정도로만 이해하시면 될 듯 하네요.


여기서 설명할 내용은, 그림에 표현된 스레드 사용 유형 중 가장 마지막에 표현된, 비동기(Asynchronous) 작업을 위한 스레드 사용에 대한 것입니다.

3. 비동기(Asynchronous) 실행.

다시 한번, 이 글에서 다루고자 하는 내용은 비동기(Asynchronous)로 실행될 필요가 있는 기능을 구현하는 방법에 대한 것입니다. 위의 그림에서 "필요에 따라 만들어진 스레드가 메인스레드와 상호 작용 후 종료"하는 형태로 동작하는 스레드 동작 유형에 해당하죠.


스레드 동작 형태 유형 - AsyncTask


그런데 "비동기(Asynchronous) 실행"이라는 것이 의미하는 것이 정확히 무엇일까요? 그리고 일반적인(?) 실행 흐름과 어떤 차이가 있으며, 스레드와는 어떤 관련이 있는 것일까요?


비동기(Asynchronous) 실행을 정확히 이해하기 위해서는, 그 반대 개념인 동기(Synchronous) 실행에 대한 기본 개념 이해가 선행되어야 합니다. 이를 통해, 비동기(Asynchronous) 실행이 왜 필요한지, 어떤 흐름으로 동작하는지 정확히 이해할 수 있습니다.

3.1 동기(Synchronous) 실행 vs 비동기 (Asynchronous) 실행.

프로그래밍 분야에서, 동기(Synchronous) 실행이 의미하는 것은, 프로그램 실행 중 하나의 기능 또는 함수를 실행했을 때, 그것을 실행한 측(Caller)에서 기능이나 함수의 모든 동작이 완료될 때까지 대기하는 방식을 의미합니다. 그리고 비동기(Asynchronous) 실행은, 기능 또는 함수의 동작이 끝나길 기다리지 않고 바로 그 다음 코드를 실행하는 방식을 말하죠.


동기 vs 비동기. (Synchronous vs Asynchronous)


예를 들어, 앱 실행 시 파일로부터 데이터를 읽는 기능을 가진 메서드를 호출하는 경우를 가정해 보겠습니다. "readFileData()"라는 이름을 가진 이 메서드를 사용하는 코드는, 아래와 같이 간단한 형태로 작성될 수 있습니다.


동기 실행 예제 1. (Synchronous 예제 1)


아주 직관적이고 익숙한, 한 눈에 알아볼 수 있는 흐름입니다.


메인 액티비티의 onCreate() 메서드에서 readFileData() 메서드를 호출하는데요. 이 때 onCreate() 메서드는 readFileData() 메서드를 호출하고 난 뒤 파일의 데이터가 모두 읽히기를 기다립니다. 그리고 readFileData() 메서드의 실행이 완료되면 그 다음 코드인 showFileData()를 실행합니다. 이러한 실행 흐름이 바로 동기(Synchronous) 실행 입니다.


자, 그런데 여기서, 한 가지 좋지 않은 상황을 가정해보겠습니다. 읽고자 하는 파일의 크기가 매우 큰 경우 말이죠. 만약 파일의 크기가 아주 커서 파일의 내용을 읽는데 몇 초, 몇 십초 이상의 시간이 소요된다면, 분명 심각한 문제가 발생할 것입니다. 왜냐하면 readFileData()에서 파일을 읽을 동안 readFileData()를 호출한 메인 스레드가 다른 이벤트들을 처리하지 못하게 될 것이고, 최악의 경우, ANR(Application Not Responding)이 발생하면서 시스템에 의해 앱의 실행이 중지되는 상황이 생길 수도 있습니다. 이는 [안드로이드 스레드 예제. 스레드로 고민해보기. (Android Thread UseCase)] 주제에서 여러 번 강조한, "안드로이드 앱의 메인 스레드에서는 무한 루프나 실행 시간이 긴 작업, 또는 과도한 대기(sleep) 등의 작업이 실행되어서는 안된다"는 원칙을 명백히 위반하는 것이죠.


동기 실행 예제 2. (Synchronous 예제 2)


이런 경우 개발자가 선택할 수 있는 방법은, 파일을 읽는 코드를 별도의 스레드에서 실행되도록 만들어 메인 스레드의 실행에 영향이 가지 않게 하는 만드는 것입니다. 즉, readFileData() 메서드 내부에서는 파일을 복사하는 새로운 스레드를 실행하기만 하고 바로 리턴되게 만드는 것이죠. 그러면 onCreate()에서 readFileData()를 호출하더라도 메인 스레드가 대기할 필요 없이 바로 다음 코드를 실행할 수 있습니다. 그리고 파일 복사 스레드에서 파일을 다 읽고나면, 메인 스레드에 메시지(Message)를 전달하여 메인 스레드에서 파일의 내용을 화면에 표시하게 만들면 됩니다. 이것이 바로 비동기(Asynchronous) 실행입니다.


비동기 실행 예제 1. (Asynchronous 예제 1)


단순히 파일을 읽는 경우를 예로 들어 비동기(Asynchronous) 실행을 설명했는데요, 사실, 앱의 기능을 구현하는데 있어 "비동기(Asynchronous) 실행"은 훨씬 다양한 상황에 적용 가능합니다. 크기가 큰 파일 복사, 데이터베이스 쿼리, HTTP 서버 API 요청, FTP 파일 다운로드 등, 비동기(Asynchronou) 실행이 필요한 경우를 찾는 것은 그리 어렵지 않죠.


요약하자면, 동기(Synchronous) 실행과 비동기(Asynchronous) 실행은, 어떤 기능을 실행한 다음, 그 기능의 실행이 완료될 때까지 대기할 것인지 여부에 따라 구분됩니다. 실행이 모두 완료될 때까지 기다리면 "동기(Synchronous) 실행", 실행이 완료되기를 기다리지 않고 바로 다음 코드를 실행하는 것은 "비동기(Asynchronous) 실행" 입니다.

3.2 비동기(Asynchronous) 실행 구현 방법.

자, "비동기(Asynchronous) 실행"에 대한 개념을 설명했으니, 이제, 실질적인 구현 방법에 대해 알아볼텐데요. 그런데 사실, "비동기(Asynchronous) 실행" 이라고 해서, 앞서 정리한 [개발자를 위한 레시피 - THREAD]를 벗어나는 내용은 없습니다. 오히려, 앞서 다른 글에서 설명한 스레드와 스레드 간 통신에 대한 내용에 정확히 들어 있다고 할 수 있죠. "메인 스레드의 실행에 영향을 줄 수 있는 기능을 새로운 스레드에서 실행하고, 기능이 완료되면 메인 스레드 핸들러로 메시지를 전달한다."는 것, 명확하지 않습니까?


그런데 안드로이드에서의 "비동기(Asynchronous) 실행"을 위한 스레드 처리 과정을 가만히 살펴보면, 공통된 패턴이 존재하는 것을 발견할 수 있습니다. 바로, 스레드에서 실행되는 작업이 완료되면 더 이상 스레드를 유지하지 않아도 되는 "단발성 실행"이라는 것과 스레드 실행 중간 상태 또는 최종 결과를 "메인 스레드로 전달"한다는 사실입니다.

비동기 실행 예제 2. (Asynchronous 예제 2)


이를 통해 "비동기(Asynchronous) 실행"의 정형화된 패턴을 나열해본다면, 아래처럼 될 수 있겠네요.


  • 실행(execute) : 비동기(Asynchronous) 작업 준비 및 시작.
  • 백그라운드 작업(doInBackground) : 백그라운드 스레드에서 비동기(Asynchronous) 작업 실행.
  • 진행 상황 업데이트(onProgressUpdate) : 백그라운드 스레드 진행 상황을 메인스레드로 전달.
  • 비동기 실행 완료 후 처리(onPostExecute) : 백그라운드 스레드 완료 후 메인스레드에 완료 상태 전달.


    execute -> doInBackground -> onProgressUpdate -> onPostExecute


자 그렇다면, 안드로이드에서 "비동기(Asynchronous) 실행" 패턴은 어떻게 구현할 수 있을까요? 스레드와 관련된 이전 글들을 참고하여 코드를 작성한다면 어떻게 구현할 수 있을까요? 아마도 [개발자를 위한 레시피 - THREAD]의 내용을 충분히 이해했다면, 스레드를 만들고 핸들러를 통해 메시지를 전달하는 과정이 그리 어렵지는 않을 것 같습니다.


그런데 안드로이드 SDK의 많은 부분이 그러하듯, 조금이라도 반복적인 구현 작업 요소가 포함되거나 작업 절차에 있어 공통적인 패턴이 존재한다면, 이는 개발자가 쉽게 사용할 수 있도록, 새로운 API로 제공됩니다.


"비동기(Asynchronous) 실행"을 위한 작업도 마찬가지이며, 여기서 설명할 AsyncTask 클래스가 바로 비동기 실행을 위해 제공되는 클래스입니다.

4. AsyncTask

AsyncTask는 그 이름에서도 알 수 있듯이, 비동기(Asynchronous)적으로 실행될 필요가 있는 작업(Task)을 위해 사용하는 클래스입니다. 특히 Thread, Handler, Message, Runnable 등을 직접 다루지 않아도, 메인 스레드와 별개로 "비동기(Asynchronous) 실행"이 필요한 작업에 사용할 수 있습니다.


AsyncTask로 비동기 작업을 구현하기 위해서는 먼저 아래의 몇 가지 사항에 대해 알아두어야 합니다.


  1. 추상 클래스 : abstract class AsyncTask.
  2. 제네릭 타입 : AsyncTask<Params, Progress, Result>
  3. 가변 인자 : (Params ...), (Progress ...)
  4. 실행 단계 : onPreExecute, doInBackground, onProgressUpdate, onPostExecute

4.1 추상 클래스. (abstract class AsyncTask)

AsyncTask는 추상 클래스(abstract class)입니다. 이 말은, AsyncTask를 사용하기 위해서는 반드시 AsyncTask를 상속(extends)한 클래스를 생성해야 한다는 것을 의미합니다. "추상 클래스"는 해당 클래스에 대한 인스턴스를 바로 생성할 수 없기 때문이죠.

public abstract class AsyncTask<Params, Progress, Result> {

}

좋습니다. abstract 키워드를 확인했으니, 구현할 때 AsyncTask를 상속한 클래스를 만들면 되겠군요. 그런데 AsyncTask가 제네릭 타입으로 선언된 것을 확인할 수 있는데요, "<Params, Progress, Result>"는 각각 무엇을 의미할까요?

4.2 제네릭 타입. (AsyncTask<Params, Progress, Result>)

AsyncTask는 "비동기(Asynchronous) 실행" 작업을 위해 사용하는 클래스입니다. 그리고 일반적으로 "비동기(Asynchronous) 실행" 작업은 "작업 시작", "작업 실행", "상태 갱신", "결과 확인"이라는 공통된 단계를 거치게 됩니다. 이러한 단계들은, 뒤에서 조금 더 자세히 설명하겠지만, 추상 클래스인 AsyncTask를 상속할 때 반드시 오버라이드해야 하는 메서드들에 매핑됩니다.


그런데 여기서 갑자기 의문이 생기네요. AsyncTask를 통해 구현하고자 하는 기능은 상황에 따라 다를테고 또 관리해야하는 데이터의 종류 또한 제각각일텐데, AsyncTask에서 구현해야 하는 메서드에서는 상황에 따라 변하는 타입의 데이터를 어떻게 전달하고 관리할 수 있을까요? AsyncTask 실행에 필요한 파라미터(예. 파일 경로, URL, DB 파일 경로 등)라던가, 현재 작업 진행 정보를 나타내는 상태 값(예. 크기 또는 개수를 나타내는 정수 값, 진행율을 위한 실수 값 등), 그리고 작업의 실행이 완료된 후의 최종 결과(예. 성공 또는 실패, 처리된 내용의 크기 또는 개수 등) 등이 상황에 따라 다르게 정의될 것은 명확한데 말이죠.


자, 그러면 이러한 상황, 즉, 구현하고자 하는 기능에 따라 클래스 내부에서 다른 타입의 데이터를 적용되게 만들어야 하는 상황에 대처하는 방법을 찾아야 하는데요, 공통된 코드를 다양한 타입에 재사용할 수 있게 만드는 방법, 바로 제네릭(Generics) 입니다.


AsyncTask는 제네릭(Generics) 클래스로 선언되어, 각 메서드에서 사용할 데이터 타입을 AsyncTask를 상속할 때 결정할 수 있도록 만들어 놓았습니다. Params, Progress, Result가 그것들이죠.


AsyncTask 제네릭 파라미터 타입


세 개의 제네릭 파라미터 타입은 이름이 가진 의미 그대로 AsyncTask에 전달될 파라미터(Params), 현재 작업 진행 상태 값(Progress), 작업 완료 최종 결과(Result)를 나타냅니다.


  • Params : AsyncTask 실행에 필요한 파라미터.
  • Progress : 현재 작업 진행 정보를 나타내는 상태 값.
  • Result : 작업의 실행이 완료된 후의 최종 결과.

제네릭 파라미터 타입을 어떻게 사용해야 하는지 선뜻 이해되시나요? 지금 당장 이해가 되지 않는다고해서 너무 걱정하실 필요는 없습니다. 아래 예제 코드를 살펴보시면, 충분히 이해되실 거라 생각합니다.


그런데 여기서 잠깐, 또 한 가지 궁금한 것이 생겼습니다. 제네릭 파라미터 타입을 결정하고나서 데이터를 전달할 때 만약 두 개 이상의 값을 사용하려면 어떻게 해야 할까요? 예를 들어 파일 복사 작업의 현재 진행 상태를 표시할 때, 복사된 파일 개수를 나타내는 Count 값과 복사된 바이트 수를 나타내는 TotalBytes를 Progress를 통해 전달해야 한다면, 두 멤버를 가지는 새로운 클래스를 만들어야 하나요? 아니면 배열 또는 컬렉션(Collections) 객체를 사용해야 하는 건가요?


아닙니다. 반드시 그렇게 할 필요는 없습니다. 왜냐하면 Params와 Progress는 doInBackground()와 onProgressUpdate() 메서드에서 각각 가변 인자(Varargs)로 전달되기 때문입니다.

4.3 가변 인자. (Varargs)

가변 인자(Varargs)는 이름 그대로, 메서드에 전달되는 파라미터의 개수가 가변적이라는 의미입니다. 메서드에 전달할 파라미터의 개수가 일정하지 않을 때 사용하는 방법인데요. 파라미터의 타입에 "..."를 추가하여 메서드에 전달되는 파라미터가 가변 인자라는 것을 명시할 수 있습니다.

    public void Func(String... vals) ;       // String 타입의 가변 인자.

    // 아래와 같이 호출 가능.
    Func("The first") ;
    Func("The first", "The second") ;
    Func("The first", "The second", "The third") ;

메서드 내부에서 가변 인자를 통해 전달된 값을 사용하는 방법은 배열을 참조하는 방법과 동일합니다.


    public void Func(String... vals) {
        if (vals.length == 1) System.out.println(vals[0]) ;
        else if (vals.length == 2) System.out.println(vals[1]) ;
        else if (vals.length == 3) System.out.println(vals[2]) ;
    }

AsyncTask의 doInBackground()와 onProgressUpdate() 메서드는 위와 같이 Params, Progress 제네릭 타입에 대한 가변 인자를 전달하도록 선언되어 있기 때문에, 다른 객체를 사용하지 않아도 두 개 이상의 값을 전달할 수 있습니다.


public abstract class AsyncTask<Params, Progress, Result> {

    protected abstract Result doInBackground(Params... params);
    protected void onProgressUpdate(Progress... values) {}
}

아래 본문의 예제에서 좀 더 구체적인 가변 인자 사용법에 대해 살펴보실 수 있습니다.

4.4 실행 단계. (onPreExecute, doInBackground, onProgressUpdate, onPostExecute)

일단 AsyncTask를 통해 "비동기(Asynchronous) 실행"이 시작되면, AsyncTask는 "작업 시작", "작업 수행", "상태 갱신", "결과 확인"의 네 단계를 거쳐 실행됩니다. 그리고 각 단계는 onPreExecute(), doInBackground(), onProgressUpdate(), onPostExecute()라는 메서드로 매핑됩니다.

AsyncTask 실행 흐름


onPreExecute(). 작업이 실행되기 직전에 UI 스레드에 의해 호출됩니다. 일반적으로 UI 초기화와 같이, "비동기(Asynchronous) 실행" 작업에 대한 초기화 과정을 수행하는 메서드입니다.


doInBackground(Params...). onPreExecute()가 호출된 뒤, 곧 바로 백그라운드 스레드에서 호출됩니다. AsyncTask가 수행할 실질적인 작업 실행 코드가 작성되는 메서드입니다. AsyncTask의 첫 번째 제네릭 파라미터 타입(Params)이 이 메서드의 파라미터 타입으로 매핑되고, 마지막 제네릭 파라미터 타입(Result)이 doInBackground()의 리턴 타입으로 매핑됩니다.
그리고 doInBackground() 실행 중 상태 업데이트가 필요한 경우, publishProgress(Progress...) 메서드를 호출하여 메인 스레드에서 onProgressUpdate(Progress...)가 호출되게 만들 수 있습니다.


onProgressUpdate(Progress...). 백그라운드 스레드에서 동작하는 doInBackground()에서 publishProgress(Progress...)를 호출하면, UI 스레드에서 호출되는 메서드입니다. 보통 현재 작업 진행 상태를 화면에 갱신하는 역할을 수행합니다.


onPostExecute(Result). 백그라운드 스레드의 모든 실행이 완료되면 UI 스레드에서 호출되는 메서드입니다. onPostExecute()의 파라미터(Result)는 doInBackground()에서 리턴되는 값입니다.


5. AsyncTask 예제.

그럼 이제 예제를 작성하면서, AsyncTask를 사용하는 방법에 대해 알아보도록 하겠습니다.


예제에서 구현하는 기능은 간단합니다. 먼저 앱의 에셋(Assets) 폴더에 파일을 하나 추가합니다. 그리고 화면의 버튼을 클릭하면 애셋(Assets)에 저장된 파일을 앱의 로컬 디렉토리에 복사하도록 만듭니다. 주로, 미리 만들어진 형식의 파일(DB, XML, JSON 등)을 앱 릴리즈 시점에 같이 배포한 다음, 앱의 로컬 디렉토리에서 읽고 쓸 수 있게 만들 때 사용하는 방법이죠.


예제 화면 구성과 동작도 아주 간단합니다. 프로그레스바(ProgressBar)와 텍스트뷰, 그리고 버튼을 하나씩 화면에 배치한 다음, 버튼을 클릭하면 AsyncTask를 통해 "비동기(Asynchronous) 실행" 작업을 수행하고 작업 진행 과정을 프로그레스바와 텍스트뷰에 표시합니다.


AsyncTask 예제 화면 구성


단, 파일 복사 과정은 매우 짧은 시간에 끝날 수 있기 때문에, 기능 동작의 식별을 위해 AsyncTask의 doInBackground() 메서드에서 파일을 복사할 때 Thread.sleep()을 사용하여 약간의 지연을 적용하였습니다.

5.1 워크플로우

AsyncTask 예제를 작성하는 절차는 아래와 같습니다.

AsyncTask 예제 구현 절차



참고로, 예제코드는 안드로이드 스튜디오 프로젝트 생성 단계에서 "Basic Activity"를 선택하여 생성된 코드를 기반으로 작성되었습니다.

5.2 Asset에 파일 추가.

AsyncTask를 구현하기에 앞서, 하나의 파일을 애셋(Assets) 폴더에 추가합니다. (예제에서는 약 500KB 크기 파일 사용). 파일의 내용은 중요하지 않으므로, 아무 파일이나 1MB 이내 크기의 파일을 사용하시면 무방할 것 같습니다.


[STEP-1] "/assets/intro.mp4" - 애셋(Assets) 폴더에 파일 추가.

애셋(Assets) 폴더에 파일 추가


참고로, 예제에서는 "mp4" 확장자("intro.mp4")를 가진 파일을 애셋에 추가했습니다. 그래서 AssetManageropenFd() API를 이용해 파일을 열 때 별다른 문제가 발생하지 않았는데요. 만약 다른 확장자를 사용하면, 아래와 같이 파일을 열 수 없다는 에러가 발생할 수 있습니다.


java.io.FileNotFoundException: This file can not be opened as a file descriptor; it is probably compressed

안드로이드 애셋에 파일을 추가하면 기본적으로 파일의 내용을 압축하여 저장하기 때문에 발생하는 문제인데요. "mp4" 확장자를 비롯하여 "jpg", "png", "mp3", "wav" 등을 포함한 다수의 이미지 또는 미디어 파일들은 압축하지 않은 원본 형태로 저장되지만, 그 외 파일들은 압축되어 저장됩니다.


만약, 임의의 확장자를 가진 파일(예. "db", "sql")을 압축되지 않은 형태로 애셋에 추가하려면, "build.gradle"(Module:app) 파일에 아래 내용을 명시하면 됩니다.

android {
    aaptOptions {
        noCompress 'db','sql'
    }
}


만약 애셋에 추가되는 모든 파일을 압축되지 않게 만드려면, 아래와 같이 "noCompress" 옵션에 빈 값을 지정하면 됩니다.

android {
    aaptOptions {
        noCompress ''
    }
}


안드로이드 애셋(Android Asset)에 대한 기본 사용법은 [안드로이드 애셋(Asset) 사용하기.]의 내용을 참고하시기 바랍니다.


5.3 메인액티비티 레이아웃 작성.

앞서 설계한 화면대로 메인액티비티 레이아웃 리소스 XML을 작성합니다.


[STEP-2] "content_main.xml" - 메인액티비티 레이아웃 리소스 XML 작성.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Press COPY button to copy DB file."
        android:id="@+id/textMessage"
        android:textSize="32sp"
        android:textColor="#FFFFFF"
        android:background="#0000FF"
        android:gravity="center"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintRight_toRightOf="parent" />

    <ProgressBar
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="0dp"
        android:layout_height="40dp"
        android:id="@+id/progressCopy"
        android:progress="0"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toBottomOf="@id/textMessage"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_margin="20dp" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/buttonCopy"
        android:text="Copy"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toBottomOf="@id/progressCopy"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_margin="20dp" />

</androidx.constraintlayout.widget.ConstraintLayout>

5.4 화면 구성 뷰에 대한 참조 획득.

화면에 배치한 뷰에 파일 복사 과정을 업데이트하기 위해, 메인액티비티의 onCreate() 메서드에서 텍스트뷰(id:textMessage)와 프로그레스바(id:progressCopy)의 참조를 획득하는 코드를 작성합니다.


[STEP-3] "MainActivity.java" - 화면에 배치된 뷰에 대한 참조 획득.
public class MainActivity extends AppCompatActivity {
    private ProgressBar mProgressCopy = null ;
    private TextView mTextMessage = null ;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        /// ... 코드 계속

        mProgressCopy = findViewById(R.id.progressCopy) ;
        mTextMessage = findViewById(R.id.textMessage) ;

        /// 코드 계속 ...
    }
}

5.5 AsyncTask 클래스 상속.

이제, 애셋 폴더의 파일을 앱 로컬 디렉토리로 복사하는 작업을 구현하기 위해 AsyncTask를 상속한 클래스를 추가합니다.


[STEP-4] "MainActivity.java" - AsyncTask를 상속한 클래스 추가.
public class MainActivity extends AppCompatActivity {
    /// 코드 계속 ...

    private class CopyDatabaseAsyncTask extends AsyncTask<String, Long, Boolean> {
        private Context mContext = null ;

        public CopyDatabaseAsyncTask(Context context) {
            mContext = context ;
        }
        // TODO : override onPreExecute(), doInBackground(), onProgressUpdate(), onPostExecute().
    }
}

AsyncTask를 상속한 CopyDatabaseAsyncTask에서는 Params, Progress, Result를 각각 String, Integer, Integer 타입으로 지정합니다. Params에는 애셋(Assets)에서 복사할 파일 이름이 전달되므로 String 타입을, Progress와 Result에는 현재 복사 중인 파일의 크기와 최종적으로 복사된 파일 크기가 전달되기 때문에 Integer 타입을 지정합니다.

5.6 onPreExecute() 메서드 오버라이드.

AsyncTask 기능 구현의 첫 번째 단계는 onPreExecute() 메서드를 오버라이드하는 것입니다.


앞서 설명했듯이, onPreExecute() 메서드는 AsyncTask의 백그라운드 스레드가 실행되기 전, 메인 스레드에 의해 호출되는 메서드입니다. 주로 UI 초기화 작업이 이루어지는 메서드죠. 예제에서도 화면 각 요소 값의 초기화를 onPreExecute()에서 수행합니다.


[STEP-5] "MainActivity.java" - onPreExecute() 메서드 오버라이드.
    private class CopyDatabaseAsyncTask extends AsyncTask<String, Long, Boolean> {

        /// ... 코드 계속.

        @Override
        protected void onPreExecute() {
            mProgressCopy.setMax(100) ;
            mProgressCopy.setProgress(0) ;
        }

        /// 코드 계속 ...
    }

5.7 doInBackground() 메서드 오버라이드.

이제 실질적인 비동기 작업이 실행되는 doInBackground() 메서드를 오버라이드합니다.


앞에서도 강조했듯이, doInBackground() 메서드는 메인스레드가 아닌, 백그라운드 스레드에서 실행되는 코드입니다. 그러므로 doInBackground() 메서드 내에서 UI를 직접 제어하면 안됩니다.


대신, publishProgress() 메서드를 사용하여 화면에 표시될 데이터를 전달하고, onProgressUpdate() 메서드에서 UI 화면을 갱신할 수 있습니다.


[STEP-6] "MainActivity.java" - doInBackground() 메서드 오버라이드.
    private class CopyDatabaseAsyncTask extends AsyncTask<String, Long, Boolean> {
        /// ... 코드 계속

        @Override
        protected Boolean doInBackground(String... params) {
            AssetManager am = mContext.getResources().getAssets() ;
            File file = null ;
            InputStream is = null ;
            FileOutputStream fos = null ;
            long fileSize = 0 ;
            long copySize = 0 ;
            int len = 0 ;

            byte[] buf = new byte[1024] ;

            try {
                fileSize = am.openFd(params[0]).getLength() ;

                is = am.open(params[0]) ;

                file = new File(getFilesDir(), params[0]) ;
                fos = new FileOutputStream(file) ;

                while ((len = is.read(buf)) > 0) {
                    fos.write(buf, 0, len) ;

                    copySize += len ;

                    publishProgress(fileSize, copySize) ;

                    // sleep 100ms.
                    Thread.sleep(10) ;
                }

                Thread.sleep(500) ;

                fos.close() ;
                is.close() ;
            } catch (Exception e) {
                e.printStackTrace() ;
            }

            return (fileSize == copySize) ;
        }

        /// 코드 계속 ...
    }

5.8 onProgressUpdate() 메서드 오버라이드.

doInBackground() 메서드에서 publishProgress() 메서드를 호출했을 때, 메인 UI 스레드에서 실행할 onProgressUpdate() 메서드를 구현합니다.


[STEP-7] "MainActivity.java" - onProgressUpdate() 메서드 오버라이드.
    private class CopyDatabaseAsyncTask extends AsyncTask<String, Long, Boolean> {
        /// ... 코드 계속

        @Override
        protected void onProgressUpdate(Long... values) {
            long fileSize = values[0] ;
            long copySize = values[1] ;
            int percent = (int)((copySize * 100) / fileSize) ;

            mTextMessage.setText(percent + " %") ;
            mProgressCopy.setProgress(percent) ;
        }

        /// 코드 계속 ...
    }

5.9 onPostExecute() 메서드 오버라이드.

마지막으로 doInBackground() 메서드 실행이 완료되어 리턴되었을 때 호출되는 onPostExecute() 메서드를 구현합니다.


[STEP-8] "MainActivity.java" - onPostExecute() 메서드 오버라이드.
    private class CopyDatabaseAsyncTask extends AsyncTask<String, Long, Boolean> {
        /// ... 코드 계속

        @Override
        protected void onPostExecute(Boolean result) {
            mTextMessage.setText("Copy completed.") ;
        }

        /// 코드 계속 ...
    }

5.10 AsyncTask 실행.

이제 AsyncTask에서 구현해야 할 메서드는 모두 작성하였으므로, 메인액티비티에서 앞서 작성한 AsyncTask를 실행하기 위해 execute() 메서드를 호출하는 코드를 작성합니다.


[STEP-9] "MainActivity.java" - onPostExecute() 메서드 오버라이드.
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        /// ... 코드 계속.

        Button buttonCopy = findViewById(R.id.buttonCopy) ;
        buttonCopy.setOnClickListener(new Button.OnClickListener() {
            @Override
            public void onClick(View v) {
                CopyDatabaseAsyncTask task = new CopyDatabaseAsyncTask(MainActivity.this) ;
                task.execute("intro.mp4") ;
            }
        });
    }
}

6. 실행 결과.

예제를 실행하면, 아래와 같은 화면이 표시됩니다.


AsyncTask 예제 실행 화면 1


화면의 "COPY" 버튼을 클릭하면, 아래 그림과 같이, 파일 복사와 함께 진행 상태가 화면에 업데이트 됩니다.


AsyncTask 예제 실행 화면 2


복사가 모두 완료되면, AsyncTask가 종료되고 아래 화면과 같이 "Copy completed" 메시지가 표시됩니다.


AsyncTask 예제 실행 화면 3


7. 참고.

.END.


ANDROID 프로그래밍/THREAD , , , , , , , , , , , , , , ,

  1. 오랜만입니다. 다시 글을 쓰시는군요. 좋은 정보 감사합니다.

  2. 네. 거의 1년만에 글을 올렸네요.
    자주 글을 올려야하는데, 이래 저래 바쁜 일(이라 쓰고 핑계거리라 읽는)이 자꾸 생기네요.

    천성이 게으른 게 가장 크고요...ㅠㅠ

    방문해 주시고, 관심가져주셔서 감사합니다.

  3. Blog Icon
    안드개발자

    거의 1년여 만이시네요. 그동안 굉장히 바쁘셨나봐요.^^
    제가 알기로는 AsyncTask는 Deprecated 되었는데 다른 비동기 방법 사용하시는 게 있으신가요?

  4. 네. 거의 1년 만이네요.

    이 글은 1년 전에 정리해놨던 내용인데, 그냥 버려버리자니 아까워서, 마무리해서 올렸습니다.

    흠.. 전 보통 비 동기 필요할 때는 스레드를 사용합니다. 과거 레퍼런스 코드를 쉽게 이용할 수 있고, 큰 문제가 없어서 간단하게 스레드로 작업합니다.

    관심가져주셔서 감사합니다.

  5. 개발하면서 정말 많이 참고하고 많은 도움이된 블로그라 구독해놨었습니다.. ㅋㅋ
    혹시 향후에 코틀린이나 크로스플랫폼 언어들에 대한 포스팅을 기대해도 될까요... 부담드리는건 아니지만 지식을 전달하고 공유하는 능력이 워낙 뛰어나셔서... ㅋㅋㅋ

    여튼 너무 반가워서 두번이나 댓글을 쓰는데 자주 뵐수있길 바랍니다! 감사합니다.

  6. 정리하고 싶은 주제들은 차고 넘치는데,
    능력이 많이 부족하네요.

    머리 속에 쌓인 지식과 경험을 글로 풀어내는 게... 생각보다 시간이 많이 걸리는 일이더라고요.

    그래도 좀 더 다양한 주제들 다뤄보도록 하겠습니다.

    빨리 이 귀차니즘을 털어내야 하는데... ㅜㅜ

    구독해주시고 관심 가져 주셔서 감사드립니다.

  7. 오랜만에 글을 쓰셨네요. ㅎㅎ.
    잘 지내시죠?

  8. 잘... 지내고 있는건지...
    잘... 모르겠습니다.
    잘... 살고 싶은데,
    잘... 난 구석이 하나도 없어서 꾸역꾸역 살고 있네요.

    sujinlab님도 잘 지내시고, 건강하십시오~

  9. 정보 감사합니다

  10. 방문해 주셔서 감사합니다.

  11. Blog Icon
    JB

    와.. 이전에는 엄청 어렵게만 느껴졌던 뽀따님 글이 너무 쉽게 읽혀서 이렇게 글 씁니다.. 정말 감사합니다. 예전에 안드로이드 프로그래밍할때 자주 봤었는데, 안드로이드에 대해 조금만 이해하고 있으면 정말 정말 좋은 글이라고 느껴집니다. 감사합니다!

  12. 칭찬 댓글 남겨주셔서 감사합니다.
    좀 더 이해하기 쉬운 내용 담을 수 있도록 노력하겠습니다.

    감사합니다.

  13. Blog Icon
    뽀따팬1호

    안녕하세요, 글 잘보고갑니다
    혹시 rxJava도 나중에 다뤄주실의향이있으신가요
    항상 중요한부분만 잘 설명해주셔서 꼭 읽어보고싶네요 ㅠ

  14. 정리하고 싶은 내용들은 차고 넘치는데...
    일단 능력이 많이 부족하네요. ㅜㅜ

    현업에 쫓기다보니, 1년 넘게 글을 못 쓰고 있어요.

    그래도 포기하지 않고 노력해보겠습니다.

    방문해 주셔서 감사합니다.

  15. Blog Icon
    뽀따팬2호

    글 너무 잘 보고 갑니다... 예제를 적용함으로써 이해도가 높아지는건 정말 큰 도움이 되는듯합니다. 글도 너무 잘써주시고 최근에 알게되어 보고있지만 앞으로 자주 방문할 예정입니다 ㅎㅎㅎ 감사합니다!!

  16. 칭찬 댓글 남겨주셔서 감사합니다!!

  17. Blog Icon
    초보 개발자 1호

    오랜만에 글 올려주셨군요. AsyncTask는 현업에서 계속 자주 사용했었는데 각각의 변수들이 어떤 역할인지, 개념은 무엇인지 등은 제대로 정리를 못 했었거든요. 오늘 확실하게 정리하게 된 것 같습니다.
    예전에 리스트뷰 관련해서 여쭤보고 도움이 많이 되었던 적 있습니다. 이번에도 좋은 글 감사드립니다.
    개발자 3년차가 되었는데 이론적으로 여기서 가장 많이 배우는 것 같습니다. 언제나 고맙습니다.

  18. 1년 전에 정리했던 내용이었는데, 그냥 버리기엔, 글 작성에 들였던 노력과 시간이 아까워 올렸어요.
    아주 조금이나마 도움이 된 것 같아 다행입니다.

    다른 내용들도 많이 도움되시면 좋겠습니다.

    감사합니다.

  19. 앱 개발을 하고있는 초보 안드 개발자인데 항상 복붙으로 남 코드 긁어서 쓰면서 어떤부분이 뭔지 제대로 모르고 있다가 이제서야 제대로 공부해보네요.
    눈으로만 읽어도 이해가 가게 글을 잘 써놓으셨네요. 앞으로 여러번 들려서 공부하고 가겠습니다~

  20. 뭔가 번지르르하지만, 많이 부족합니다.
    다른 블로그 내용들도 잘 참고하셔서, 고수 안드 개발자가 되시길 바랄게요.

    감사합니다.

  21. Blog Icon
    뽀따사랑

    뽀따님께서 올려주신 안드로이드 강의로 개념 잡는데 정말 많은 도움이 되었습니다!!
    이 글까지 갓뽀따님께서 올려주신 안드로이드 관련 글 거의 다 정독한 것 같네요ㅎㅎ
    제가 자바로 계속 개발을 해오고 있는데,, 코틀린을 배워야겠죠..?

  22. 네. 코틀린으로 넘어가는 게 좋을 것 같네요. 구글에서 그렇게 방향을 정했으니, 따라가는 게 맞다고 생각합니다.

    블로그에 관련 내용을 업데이트 하면 좋겠지만...

    제가 요즘 Node Backend + React Frontend 환경에서 작업 중이라, 안드로이드를 살펴볼 겨를이 잘 없네요.

    여태까지 열심히 하셨으니, 코틀린으로 개발하시는 것도 크게 어렵지 않으시리라 생각합니다.

    감사합니다.

  23. Blog Icon
    뽀따사랑

    답변 감사합니다!!
    질문 하나만 더 드릴게요!
    안드로이드 쪽을 어느정도 마치면 크로스플랫폼으로 가는게 좋을까요? 네이티브 IOS 쪽을 먼저 하는게 좋을까요?

  24. 질문하신 내용에, 제가 답변을 드리는 게 의미가 없을 것 같아요.

    전 모바일을 전문적으로 하는 개발자가 아니기 때문에, 모바일 생태계에 대한 관심이 크지 않습니다. 트렌드를 포괄적으로 파악하고 있지 않기 때문에, 어떤 답변이든 간에 도움이 되지는 않을 것 같습니다.

    명확한 답을 드리지 못해 죄송합니다.
    감사합니다.

  25. Blog Icon
    뽀따사랑

    답변 감사합니다~

  26. 관심 가져주셔서 감사합니다.

  27. 너무 좋아요 ㅜㅜ 강의 듣고 싶을 정도.. 감사합니다 !

  28. 칭찬글 남겨주셔서 감사합니다!!

  29. Blog Icon

    비밀댓글입니다

  30. 제가 사용하고 있는 스킨은 아래 링크에서 확인하실 수 있습니다.

    https://tyzen.tistory.com/199

    해당 스킨은 더 이상 유지보수 안하고 있는 것으로 알고 있는데, 저도 그 이상 자세한 내용은 잘 모르겠네요.

    컨텐츠나 이미지를 제외한 여러 양식들은 제가 만든 것들이 아니라 무료로 오픈된 것을 사용했기 때문에 카피해서 사용하셔도 무방합니다.

    감사합니다.

  31. Blog Icon
    James

    글 항상 잘보고 있습니다!!

  32. 방문해 주셔서 감사합니다.

  33. Blog Icon
    Kim

    안녕하세요 좋은 글 정말 잘봤습니다ㅎㅎ 염치불구하고 질문 한가지만 드려도 될까요?
    Async 는 애뮬레이터를 실행하고 한번만 동작하고 끝나더라구요..
    task.execute("intro.mp4")
    이렇게 execute를 하고 뒤로가기를 해서 다시 동작하게 하고 싶은데 잘 안되네요..
    task.cancle(true) 이런식으로 cancle을 하고 다시 실행시켜도 안됩니다.
    혹시 Async를 여러번 실행시킬 수 있는 방법이 있을까요?

  34. 한번만 동작하고 끝나지는 않는데요.
    아마 코드를 조금 잘못 작성하신게 아닌가 생각이 드네요.
    AsyncTask와 관련된 코드를 일부 올려주시면 원인을 찾는데 도움을 드릴 수 있을 것 같습니다.

    감사합니다.