------------------------------------------------------------------------------------------------
출처: http://tigerwoods.tistory.com/26
http://tigerwoods.tistory.com/28
------------------------------------------------------------------------------------------------
1. 스레드(Thread) 사용의 필요성
A. UI 스레드 (Thread)의 중요성
안드로이드에서 어플리케이션이 구동되면 main이라 불리는 스레드가 하나 생성된다.
main 스레드는 UI 스레드(앞으로 이렇게 표기)라고도 불리며, 어플리케이션의 로직과 UI 간의
상호작용을 돕고, 발생한 이벤트들을 위젯에게 전달해 처리 할 수 있게 하는 등 매우 중요한 역할을
담당한다.
예를 들면, 사용자가 버튼을 터치 했을 때 다음과 같은 일이 발생한다.
- UI 스레드가 터치 이벤트를 버튼 위젯에게 전달한다.
- 버튼 위젯은 자기 자신의 상태를 눌림(press)상태로 바꾸고 필요한 작업을 진행한다.
- 버튼 위젯은 자기 자신을 다시 그리라는 메시지(invalidate)를 이벤트 큐(queue)에 등록한다.
- UI 스레드는 이벤트 큐에 등록된 request를 버튼 위젯에게 전달한다.
- 버튼 위젯은 자기 자신의 영역을 버튼 눌림 상태로 다시 그린다.
지금까지의 포스트 전에 사용한 모든 예제는 UI 스레드에서 모든 작업을 수행해왔다.
아주 간단한 작업들 이였기 때문에 별 문제가 없었지만 복잡한 작업(시간이 오래 걸리는)을
UI 스레드에서 수행하는 경우 어떤 상황이 발생하는지 보자.
B. UI 스레드에서 시간이 오래 걸리는 작업의 수행
어플리케이션을 개발하다 보면 어떤 작업들은 계산의 복잡도 때문에 또는 작업에 필요한 리소스가
늦게 준비되는 등의 이유로 리턴 시간이 오래 걸리는 경우가 많이 있다. 수학적으로 아주 복잡한 계산이
필요하거나, DB로부터 많은 정보를 끌어 올 때 등과 같은 경우에 수초에서 수분이 걸리는 작업을
수행해야 될 경우가 있다.
예를 들어 인터넷 사이트 상에서 용량이 적은 이미지를 다운 받아 화면에 표시하는 작업을 UI 스레드에서 구현 했다. 현재 네트워크 상황이 좋지 않아 이미지를 다운 받는데 20초 정도가 걸린다고 가정해 보자.
이미지를 다운받기 시작한 후 10초 정도 경과했을 때 기다리다 지친 사용자는 '취소' 버튼을 눌렀다.
하지만 사용자가 버튼을 눌렀을 때 발생하는 버튼 클릭 메시지를 취소 버튼에 전달하는 역할을 하는
UI 스레드는 이미지 다운로드 루틴에서 아직 리턴 하지 않았으므로 사용자가 새로 발생시킨 메시지를
처리 할 수 없는 상황이 발생해 버린다.
결과적으로 사용자는 중간에 취소 하고 싶어도 취소 할 수 없을 뿐 더러 20초 동안 아무 반응도 하지 않는 화면을 바라보고 있어야만 한다. 심지어는 이 시간 동안 전화가 온다고 하더라도 전화를 받지 못할 수도
있다.
안드로이드에서는 이런 상황을 방지 하기 위해 UI 스레드가 5초 이상 반응을 하지 않는 어플리케이션의 경우 'Application Not Responding(ANR)' 다이얼로그를 팝업 시켜 사용자가 어플리케이션을 강제 종료 해 버릴 수 있도록 하고 있다.
이런 극단적인 방법을 쓰는 이유는 UI가 사용자와 디바이스간의 유일한 소통 창구이며 특정 어플리케이션의 문제가 디바이스의 전반적인 운영(전화, 문자 수신 등등)에 큰 악영향을 미칠 수 있기 때문이다.
사용자가 마음대로 작동할 수 없는 디바이스는 고장 난 회로 덩어리에 불과할 뿐이다.
예제를 보면서 이런 경우를 한번 재현 시켜 보자.
이 번 포스트의 예제는 Progress bar 한 개와 몇 개의 버튼으로 이루어 져 있는데, 버튼을 누르면
약 6초 정도가 걸리는 작업을 UI 스레드에서 실행해 문제를 일으키거나 다양한 스레드 기법을 사용하여
문제를 해결하기도 하는 것을 보여준다. 실재 어플리케이션에서는 worker 스레드 내부는 복잡한 계산이나 DB querying, 자원 다운로드 같은 것을 수행하겠지만 본 예제에서는 SystemClock.sleep(float)
메소드로 0.6초간 시간을 때우다 progress bar를 10% 채우는 것을 100%가 될 때까지 반복한다.
예제는 다음과 같은 초기 화면 구성을 하고 있다.
'01. Thread 사용안함' 버튼을 누르면 두 번째 그림처럼 버튼이 클릭된 상태로 6초간 디바이스의
모든 동작이 정지한것 처럼 보인다 (UI 스레드가 block 되었다고 표현한다).
이 동안은 현재 실행 중인 작업을 중단하거나, 어플리케이션을 종료하거나, 심지어 전화를 받는 작업조차 불가능 하며, 6초 정도가 지나면 block이 풀리면서 Progress Bar가 한번에 100%로 변해 버린다.
이 6초 동안 다른 버튼을 클릭하는 등 UI를 사용하면 메시지를 바로 해당 UI 위젯에 전달 할 수 없으므로
(복잡한 계산을 수행 중) 메시지 queue에 보관된다. UI 스레드가 복잡한 작업을 완료 후 메시지 queue 에 있는 다음 메시지를 처리할 때 5초 이상 대기한 메시지가 있다면 안드로이드는 다음과 같은 ANR 창을 팝업 시켜 사용자에게 문제가 있는 어플리케이션임을 알리고 강제종료 시킬지 계속 기다릴지 선택할 수 있게 한다.
※ ANR 상황을 재현하기 위해서는 예제에서 1번 버튼을 클릭하자 마자 2번 버튼을 클릭하고
작업이 완료되기를 기다린다. 1번 버튼만 클릭하고 아무것도 안하고 기다리면 ANR창이 안 나타난다.
이와 같이 시간이 오래 걸리는 작업은 UI thread가 아닌, worker / background 라고 불리는
별도의 스레드에서 실행 함으로 UI 스레드의 block을 해결할 수 있다.
이제부터 안드로이드에서는 어떻게 스레드(thread)를 구현하는지 살펴 보자.
2. 스레드(Thread) 구현에 필요한 것들
A. Thread 객체
Thread 는 어떤 작업들을 병렬로 실행 가능하게 하는 자바 객체이다.
각 Thread는 CPU로부터 타임 퀀텀이라는 사람이 느낄 수 없는 정도로 짧은 CPU 점유 시간을 각각
번갈아 가면서 할당 받아 자신에게 할당된 작업을 진행함으로 엄밀한 의미에서는 진짜 병렬수행은
아니지만 인간의 관점에서 보면 너무 빨리 이런 일이 진행 되기 때문에 2개 이상의 작업이 동시에
일어나는 것처럼 보인다.
Thread는 자신이 실행할 메소드, 인자, local 변수를 위한 별도의 callstack을 가진다.
같은 VM내부 (안드로이드에서 각각의 어플리케이션은 별도의 VM을 할당 받는다)의 Thread들은
상호작용(interact)이 가능하며, Thread 자신이 제공하는 여러 메소드나 Object로부터 상속하는
여러 객체를 사용해 Thread간의 동기(Synchronization)를 유지할 수 있다.
(스레드의 동기화는 안드로이드만의 주제가 아님으로 깊은 설명은 추후 기회가 된다면 다루려고 한다)
안드로이드에서 Thread 객체를 사용하는 방법은 2가지가 있는데,
첫째는 Thread 클래스를 subclassing하는 새 객체를 정의해고 Thread:run() 메소드를 오버라이딩 하는
방법이고,
둘째는 new 연산자로 새로운 Thread 객체를 생성하면서 생성인자로 Runnable 인터페이스
(스레드에서 실행될 logic포함)를 전달하는 방식이다.
데모에서는 두 번째 Runnable 인터페이스를 사용하는 방식을 사용했다.
B. Runnable 인터페이스
Runnable 인터페이스는 위와 같은 상속 구조를 가지고 있으며,
딱 하나의 abstract 메소드를 제공 하는데, 바로 run()이란 메소드이다.
새 Work 스레드 생성시 Thread객체의 생성인자로 전달되는 Runnable 인터페이스의 run() 메소드는
새 work 스레드가 실행할 작업을 포함해야하며, run()은 생성된 새 work 스레드가 시작되면 자동으로
호출된다.
3. 스레드(Thread)에서의 UI 업데이트
위에 설명한 Thread와 Runnable을 사용해 다음과 같이 별도의 work 스레드를 구현 한다면 UI blocking
문제를 해결 할 수 있다. 하지만 한가지 주의할 점이 있으니 살펴보자.
잠재적 문제를 가지는 Thread 구현
public void onClick(View v) { if else (v == btnThread01) { ......
|
위의 코드를 실행 시켜 보면 6초간에 걸쳐 10%씩 순차적으로 100%까지 증가하는 정상적인 progress bar가 구현된 것을 볼 수 있다. 또 전 예제와 다르게 UI가 block되지 않아 다른 UI이벤트에 대해서도
즉각 반응함을 볼 수 있다. 하지만 위의 예제는 잠재적으로 심각한 오류를 일으킬 소지를 가지고 있다.
안드로이드 UI 위젯들은 스레드 세이프 (Threadsafe)하게 디자인 되지 않았기 때문이다.
만약 두 개 이상의 스레드가 동시에 UI위젯 자원에 접근하여 이를 조작 하려고 한다면 개발자가 예상하지 못한 결과가 일어날 수도 있다. 여기서 사용된 ProgressBar객체는 별 문제 없이 작동하지만, UI스레드에서 생성된 TextView가 worker 스레드에서 직접 컨트롤 되면, run-time exception이 발생하며 exception 처리가 안되어 있을 경우 어플리케이션이 강제 종료 될 수도 있다.
사용자에게 좀 더 낳은 UX(User eXperience)를 제공하기 위해 사용된 Thread가 자칫 잘못 사용되면
어플리케이션의 안정성을 해치기 때문에 올바르게 구현하는 것이 중요하다.
Worker 스레드에서 실행된 결과가 UI 위젯에 안전하게 반영되기 위해서는 어떻게 코드를 작성해야
하는지 다음 단락에서 살펴보자.
4. 올바른 스레드(Thread) 구현 방법
안드로이드는 복수의 스레드가 하나의 UI 위젯에 동시에 접근해서 일어날 수 있는 잠재적인 문제를
해결하기 위해 다음과 같은 절차로 work스레드가 UI위젯에 접근하게 끔 어플리케이션을 구성한다.
- worker 스레드가 UI 위젯에 적용할 작업을 메시지 queue에 추가 함
- UI 스레드가 메시지를 dispatch해 해당 UI위젯에 메시지를 전달 함
- UI 위젯은 메시지 대로 자신의 상태를 update함.
정리하면 하나의 자원에 동시에 접근할 수 있는 스레드를 하나로 제한해서 혼선을 줄이는 것이 핵심이다. 이를 그림으로 표현하면 다음과 같다.
(물론 일반적인 스레드 동기화 방법 mutex, semaphore, critical section등을 사용할 수도 있다).
안드로이드에서는 위와 같이 UI스레드만 UI위젯에 접근하게 하기 위해 3가지 방법을 사용 할 수 있는데 각각 다음과 같다.
A. Thread 구현 예 01: View:post(…) 이용
View 클래스는 post() 메소드를 제공 하는데, 메소드의 설명은 다음과 같다.
parameter
action: 메시지 queue에 queue될 Runnable 객체.
return
true: Runnable객체가 메시지 queue에 성공적으로 queue됨.
false: Runnable 객체를 메시지 queue에 queue하는데 실패함.
View:post(…) 메소드는 다음과 같이 사용 가능하다.
|
위의 코드에서 보면 2개의 Runnable 객체가 사용되는데 목적을 정리하면 다음과 같다.
새 Thread의 생성인자로 전달되는 Runnable: Thread가 start() 메소드에 의해 시작되면
자동 실행됨. 이 Runnable 내부에서 bar.post()와 sleep을 실행함.
bar.post(…)의 인자로 전달되는 Runnable: UI 스레드의 메시지 큐에 메시지
(ProgressBar 인스턴스 bar를 증가 시킴)를 등록 함.
결과적으로, 새로 생성된 work 스레드의 역할은 긴 시간이 필요한 작업을 처리 후 특정 자원
(UI 위젯 등)의 상태 update 명령을 메시지 큐에 등록하는 것까지이고, 저장된 메시지가
UI 위젯에 전달 되는 시기는 전적으로 UI스레드의 상태에 따라 결정되게 된다.
우선 순위가 높은 작업(전화, 문자 수신 등)의 메시지가 갑자기 발행하면 저장된 메시지 (UI위젯 update)의 앞으로 등록(새치기) 시켜 우선 적으로 처리 함으로 디바이스의 신뢰성을 높일 수 있다.
B. Thread 구현 예 02: Activity:runOnUiThread(…) 이용
또 다른 방법은 Activity가 제공하는 runOnUiThread(…) 메소드를 사용하는 방법이다.
우선 runOnUiThread(…) 메소드의 설명을 살펴보자.
action: 바로 실행되거나 메시지 큐에 등록될 메시지 (runOnUiThread()가 call 되는 위치에 따라 다름)
코드 내부에서는 다음과 같이 사용 가능하다.
|
runOnUiThread메소드의 특징은 자신이 어디서 call 되었는지에 따라 처리 방법이 다른 것인데,
UI스레드 내부에서 call되었으면 인자로 제공된 Runnable이 바로 실행되고, UI 스레드가 아니라면
메시지 큐에 등록 시켜서 UI스레드의 스케쥴에 맞춰 실행 할 수 있게 처리한다.
예제에서는 별도의 work 스레드 안에서 사용되었으므로 바로 실행되지 않고 메시지 큐에 등록된다.
C. Thread 구현 예 03: Handler객체 이용
마지막으로 android.os.Handler객체를 사용한 Thread구현 방법이 있다.
우선 Handler의 상속 구조는 다음과 같다.
Handler객체에서 메시지 큐에 메시지를 추가하는 메소드는 다음 두 가지 이다.
parameter
r: 메시지 큐에 추가 할 Runnable.
return
true: 메시지 큐에 성공적으로 r 추가
false: r을 메시지 큐에 추가 실패
코드 내부에서 사용하는 방법은 다음과 같다.
Handler객체를 사용한 스레드구현 01 ☞ post (…) 이용
private ProgressBar mProgress; private Handler mHandler = new Handler();
protected void onCreate(Bundle icicle) { setContentView(R.layout.progressbar_activity); mProgress = (ProgressBar) findViewById(R.id.progress_bar);
// 오래 걸리는 작업을 work 스레드에서 실행 함 // PorgressBar인스턴스 update 메시지를 메시지 큐에 등록
|
다음은 두 번째 방법이다.
parameter
msg: 메시지 큐에 추가될 메시지.
return
true: msg가 성공적으로 메시지 큐에 추가 됨
false: msg가 메시지 큐에 추가 되지 않음
sendMessage(Message)를 사용할 경우는 다음과 같이 구현한다.
Handler객체를 사용한 스레드구현 02 ☞ sendMessage (…) 이용
private ProgressBar mProgress;
// Handler 객체를 생성하고
protected void onCreate(Bundle icicle) { setContentView(R.layout.progressbar_activity); mProgress = (ProgressBar) findViewById(R.id.progress_bar);
// 오래 걸리는 작업을 work 스레드에서 실행 함 // 메시지를 발생시킴. 메시지 큐에 저장되며
|
지금까지 스레드의 필요성과, 스레드 구현 시 주의할 점, 안전한 스레드 구현 방법 등을 살펴 보았다.
크게 문제는 없지만 코드가 상당히 복잡해 지는 단점이 있었다.
다음 포스트에서는 AsyncTask 객체를 통해 좀더 간단한 코드로 스레드를 구현하는 방법에 대해
살펴 보려고 한다.
전 포스트에서 설명했던 여러 스레드 구현방법들은 비록 아무 문제가 없지만 구현방법이 복잡해서
코드를 읽기 힘들게 만드는 경향이 있었다. Background작업에 관한 모든 사항(스레드 객체 생성, 사용,
UI 스레드와 통신 등)이 Activity 코드에 포함 되고 특히 background 스레드가 UI위젯과 빈번한 통신을
할수록 Activity 코드의 복잡함은 점점 배가 된다.
안드로이드에서는 이런 문제를 해결하기 위해 API level 3 (1.5 version) 부터 AsyncTask라는 클래스를 제공하고 있다.
AsyncTask클래스는 background작업을 위한 모든 일(스레드생성, 작업실행, UI와 통신 등)을 추상화 함으로써 각각의 background작업을 객체 단위로 구현/관리 할 수 있게 하는것이 목적이다.
그림으로 표현하면 다음과 같다.
|
참고로 1.0과 1.1 version의 API를 사용하는 디바이스에서는 구글 code에 공개되어 있는 UserTask 라는 클래스를 어플리케이션 프로젝트에 복사해 넣어 사용할 수 있다. 기능과 사용법은 AsyncTask와 완전히 동일하다.
그럼 AsyncTask에 관해 자세히 살펴보자.
1. AsyncTask 클래스 소개
AsyncTask라는 클래스 이름은 Asynchronous Task의 줄임이며, UI스레드의 입장에서 볼 때 비 동기적으로 작업이 수행되기 때문에 붙여진 이름이다.
(Ajax: Asynchronous javascript and XML 에서 사용된 의미와 같다)
AsyncTask의 상속관계는 다음과 같다.
Object로부터 상속하는 AsyncTask는 Generic Class이기 때문에 사용하고자 하는 type을 지정해야
한다.
AsyncTask클래스의 사용시 지정해야 하는 generic type은 각각 다음의 용도로 사용된다.
- Params: background작업 시 필요한 data의 type 지정
- Progress: background 작업 중 진행상황을 표현하는데 사용되는 data를 위한 type 지정
- Result: background 작업 완료 후 리턴 할 data 의 type 지정
그림으로 각 generic type이 결정하는 것들을 표현하면 다음과 같다.
(각 메소드의 자세한 정보는 다음 단락의 예제 코드와 설명 참조)
만약 type을 정할 필요가 없는 generic이 있다면 void를 전달하면 된다.
예. …AsyncTask<void, void, void> {…}
2. AsyncTask의 사용
우선 AsyncTask가 어떻게 사용되는지 예제 소스를 보자.
AsyncTask 클래스의 사용 예
import android.app.Activity;
public class AsyncTaskDemo extends Activity implements View.OnClickListener {
// UI 스레드에서 AsynchTask객체.execute(...) 명령으로 실행되는 callback
|
AsyncTask 클래스는 다음과 같이 중요한 callback들을 제공 함으로 상황에 맞게 오버라이딩 해야 한다.
- protected void onPreExecute(): Background 작업이 시작되자마자 UI스레드에서 실행될 코드를 구현해야 함. (예. background 작업의 시작을 알리는 text표현, background 작업을 위한 ProgressBar popup등)
- protected abstract Result doInBackground(Params… params): Background에서 수행할 작업을 구현해야 함. execute(…) 메소드에 입력된 인자들을 전달 받음.
- void onProgressUpdate(Progress... values): publishProgress(…) 메소드 호출의 callback으로 UI스레드에서 보여지는 background 작업 진행 상황을 update하도록 구현함. (예. ProgressBar 증가 등)
- void onPostExecute(Result result): doInBackground(…)가 리턴하는 값을 바탕으로 UI스레드에 background 작업 결과를 표현하도록 구현 함. (예. background작업을 계산한 복잡한 산술식에 대한 답을 UI 위젯에 표현함 등)
- void onCancelled(): AsyncTask:cancel(Boolean) 메소드를 사용해 AsyncTask인스턴스의 background작업을 정지 또는 실행금지 시켰을 때 실행되는 callback. background작업의 실행정지에 따른 리소스복구/정리 등이 구현될 수 있다.
또, AsyncTask 클래스는 background 작업의 시작과 background 작업 중 진행정보의 UI스레드 표현을 위해 다음과 같은 메소드를 제공한다.
- final AsyncTask<…> execute(Params… params): Background 작업을 시작한다.
꼭 UI스레드에서 호출하여야 함. 가변인자를 받아들임으로 임의의 개수의 인자를 전달할 수
있으며, 인자들은 doInBackground(…) 메소드로 전달된다.
- final void publishProgress(Progress... values): Background 작업 수행 중 작업의
진행도를 UI 스레드에 전달 함. doInBackground(…)메소드 내부에서만 호출.
위의 메소드들은 AsyncTask 클래스를 이용해 구현된 background 작업 시 다음과 같은 형태로
사용된다.
위 의 그림에서 처럼AsyncTask인스턴스는 자기 자신을 pending, running, finished 이렇게 세 가지
상태(status)로 구분하는데 각각 AsyncTask:Status 클래스에 상수 PENDING, RUNNING, FINISHED 로
표현 될 수 있다.
현재 AsyncTask인스턴스의 상태는 다음 메소드를 호출해서 얻을 수 있다.
return
AsyncTask인스턴스의 상태정보를 AsyncTask.Status 객체의 상수 값 PENDING, RUNNING, FINISHED 중에서 리턴.
또, AsyncTask클래스는 background 작업을 정지, 또는 시작금지 시키기 위해 다음 메소드를 제공한다. 이 메소드가 성공적으로 호출되면 onCacelled() callback이 호출되니 onCacelled()에 적절한 뒤처리를 해주어야 한다.
parameter
mayInterruptIfRunning: true값을 제공했을 때 background작업이 실행 중일 경우(running 상태) 작업을 중단 시키고, 준비 중(pending 상태) 일 경우 작업을 실행 금지 시킴. (execute() 명령 사용 불가. 사용하면 exception 발생)
return
true: background작업을 성공적으로 중지하거나 실행 금지 시킴
false: 벌써 작업이 완료된 상태(finished 상태) 일 경우 리턴
마지막으로 AsyncTask 사용해 background작업을 구현 시 꼭 지켜야 하는 사항들이다.
- AsyncTask클래스는 항상 subclassing 하여 사용하여야 한다.
- AsyncTask 인스턴스는 항상 UI 스레드에서 생성한다.
- AsyncTask:execute(…) 메소드는 항상 UI 스레드에서 호출한다.
- AsyncTask:execute(…) 메소드는 생성된 AsyncTask 인스턴스 별로 꼭 한번만 사용 가능하다. 같은 인스턴스가 또 execute(…)를 실행하면 exception이 발생하며, 이는 AsyncTask:cancel(…) 메소드에 의해 작업완료 되기 전 취소된 AsyncTask 인스턴스라도 마찬가지이다.
그럼으로 background 작업이 필요할 때마다 new 연산자를 이용해 해당 작업에 대한 AsyncTask 인스턴스를 새로 생성해야 한다.
- AsyncTask의 callback 함수 onPreExecute(), doInBackground(…), onProgressUpdate(…), onPostExecute(…)는 직접 호출 하면 안 된다. (꼭 callback으로만 사용)
'IT_Programming > Android_Java' 카테고리의 다른 글
[안드로이드] 세로 스크롤이 되는 갤러리 (0) | 2011.07.08 |
---|---|
FaceBook API 연동 (0) | 2011.07.04 |
[펌] 안드로이드 App과 Twitter API 연동하기 (0) | 2011.07.01 |
[펌]안드로이드의 Touch Event 디스패치 단계 (0) | 2011.06.14 |
[android] 비트맵 이미지 애니메이션 구현하기 (0) | 2011.06.01 |