ANDROID 프로그래밍/THREAD

안드로이드 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)로 실행될 필요가 있는 기능을 구현하는 방법에 대한 것입니다. 위의 그림에서 "필요에 따라 만들어진 스레드가 메인스레드와 상호 작용 후 종료"하는 형태로 동작하는 스레드 동작 유형에 해당하죠.



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


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

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

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



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



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


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


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



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



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


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

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

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


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


이를 통해 "비동기(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에 전달될 파라미터(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()라는 메서드로 매핑됩니다.


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의 doInBackground() 메서드에서 파일을 복사할 때 Thread.sleep()을 사용하여 약간의 지연을 적용하였습니다.

5.1 워크플로우

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



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

5.2 Asset에 파일 추가.

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


[STEP-1] "/assets/intro.mp4" - 애셋(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. 실행 결과.

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



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



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



7. 참고.

.END.