Thread 구현하기 1/2 (with ProgressBar)
Android/Tips 2014. 7. 24. 17:23 |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 blcking 문제를 해결 할 수 있다. 하지만 한가지 주의할 점이 있으니 살펴보자.
잠재적 문제를 가지는 Thread 구현
01 | public void onClick(View v) { |
02 | ...... |
03 |
04 | if else (v == btnThread01) { |
05 | // 새로운 스래드와 내부에서 실행될 Logic을 Runnable로 객체화 함. |
06 | Runnable increaseProgress = new Runnable() { |
07 | |
08 | // Runnable interface의 유일한 abstract method 구현. |
09 | // run() 메소드는 본 Runnable객체가 공급된 Thread가 시작할때 자동으로 호출. |
10 | public void run() { |
11 | for (nProgress = 0 ; nProgress<= 100 ; nProgress+= 10 ) { |
12 | try { |
13 | bar.setProgress(nProgress); |
14 | } |
15 | catch (Exception e) { |
16 | Log.w( "h-exception" , e.toString()); |
17 | } |
18 | SystemClock.sleep( 600 ); |
19 | } |
20 | } |
21 | }; |
22 | |
23 | |
24 | // 위에 선언한 Runnable객체에 구현된 logic을 실행할 새로운 Thread 객체 생성 |
25 | Thread worker = new Thread(increaseProgress); |
26 | |
27 | // 새로운 스레드 시작됨 -> Runnable interface의 run()메소드가 |
28 | // 새로운 스레드에서 실행됨. |
29 | worker.start(); |
30 | } |
31 |
32 | ...... |
33 | } |
위의 코드를 실행 시켜 보면 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(…) 메소드는 다음과 같이 사용 가능하다.
01 | public void onClick(View v) { |
02 | // ...... |
03 | else if (v == btnThread02) { |
04 | new Thread( new Runnable() { |
05 | @Override |
06 | public void run() { |
07 | for (nProgress = 0 ; nProgress<= 100 ; nProgress+= 10 ) { |
08 | |
09 | // ProgressBar 객체는 View로 부터 상속 함. |
10 | // post 메소드는 UI 스레드의 메시지 큐에 bar객체에게 전달할 |
11 | // Runnable을 queue함. |
12 | bar.post( new Runnable() { |
13 | public void run() { |
14 | // UI 스레드 메시지 큐에 저장될 메시지의 핵심 |
15 | bar.setProgress(nProgress); |
16 | } |
17 | }); |
18 | // 실제로는 복잡한 계산등 시간이 소모되는 작업 수행 되겠지만 |
19 | // 데모에서는 0.6초간 sleep함 |
20 | SystemClock.sleep( 600 ); |
21 | } |
22 | } |
23 | }).start(); // 스레드 실행 |
24 | } |
25 | //...... |
26 | } |
위의 코드에서 보면 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(…) 메소드의 설명을 살펴보자.
Parameter
action: 바로 실행되거나 메시지 큐에 등록될 메시지 (runOnUiThread()가 call 되는 위치에 따라 다름)
코드 내부에서는 다음과 같이 사용 가능하다.
01 | public void onClick(View v) { |
02 | //...... |
03 | else if (v == btnThread03) { |
04 | new Thread( new Runnable() { |
05 | @Override |
06 | public void run() { |
07 | for (nProgress = 0 ; nProgress<= 100 ; nProgress+= 10 ) { |
08 | // 현재 UI 스레드가 아님으로 메시지 큐에 Runnable을 등록 함 |
09 | runOnUiThread( new Runnable() { |
10 | public void run() { |
11 | // 메시지 큐에 저장될 메시지의 핵심 |
12 | bar.setProgress(nProgress); |
13 | } |
14 | }); |
15 | // 복잡한 작업 simulating |
16 | SystemClock.sleep( 600 ); |
17 | } |
18 | } |
19 | }).start(); |
20 | } |
21 | //...... |
22 | } |
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(…) 이용
01 | public class MyActivity extends Activity { |
02 |
03 | private ProgressBar mProgress; |
04 | private int mProgressStatus = 0 ; |
05 |
06 | private Handler mHandler = new Handler(); |
07 |
08 | protected void onCreate(Bundle icicle) { |
09 | super .onCreate(icicle); |
10 |
11 | setContentView(R.layout.progressbar_activity); |
12 |
13 | mProgress = (ProgressBar) findViewById(R.id.progress_bar); |
14 |
15 | // 오래 걸리는 작업을 work 스레드에서 실행 함 |
16 | new Thread( new Runnable() { |
17 | public void run() { |
18 | while (mProgressStatus < 100 ) { |
19 | mProgressStatus += 10 ; |
20 |
21 | // PorgressBar인스턴스 update 메시지를 메시지 큐에 등록 |
22 | mHandler.post( new Runnable() { |
23 | public void run() { |
24 | // work 스레드 내부에서 명령이 실행되는 것이 아니라 |
25 | // post(..)에 의해 메시지 큐에 추가됨. |
26 | mProgress.setProgress(mProgressStatus); |
27 | } |
28 | }); |
29 | } |
30 | } |
31 | }).start(); |
32 | } |
33 | } |
다음은 두 번째 방법이다.
parameter
msg: 메시지 큐에 추가될 메시지.
return
true: msg가 성공적으로 메시지 큐에 추가 됨
false: msg가 메시지 큐에 추가 되지 않음
sendMessage(Message)를 사용할 경우는 다음과 같이 구현한다.
Handler객체를 사용한 스레드구현 02 – sendMessage(…) 이용
01 | public class MyActivity extends Activity { |
02 |
03 | private ProgressBar mProgress; |
04 | private int mProgressStatus = 0 ; |
05 |
06 | // Handler 객체를 생성하고 |
07 | // handleMessage(Message)를 오버라이딩하여 |
08 | // mHandler 인스턴스에게 전달 되는 모든 메시지를 처리함. |
09 | private Handler mHandler = new Handler(){ |
10 | @Override |
11 | // 어떤 메시지가 전달되어도 progress bar를 설정함. |
12 | public void handleMessage(Message msg) { |
13 | mProgress.setProgress(mProgressStatus) |
14 | } |
15 | }; |
16 |
17 | protected void onCreate(Bundle icicle) { |
18 | super .onCreate(icicle); |
19 |
20 | setContentView(R.layout.progressbar_activity); |
21 |
22 | mProgress = (ProgressBar) findViewById(R.id.progress_bar); |
23 |
24 | // 오래 걸리는 작업을 work 스레드에서 실행 함 |
25 | new Thread( new Runnable() { |
26 | public void run() { |
27 | while (mProgressStatus < 100 ) { |
28 | mProgressStatus += 10 ; |
29 |
30 | // 메시지를 발생시킴. 메시지 큐에 저장되며 |
31 | // UI 스레드는 메시지를 mHandler 객체로 전달함. |
32 | // 결과적으로 UI 스레드에서 |
33 | // mHnadler의 handleMessage(Message)에 의해 처리됨. |
34 | mHandler.sendMessage(mHandler.obtainMessage()); |
35 | } |
36 | } |
37 | }).start(); |
38 | } |
39 | } |
지금까지 스레드의 필요성과, 스레드 구현 시 주의할 점, 안전한 스레드 구현 방법 등을 살펴 보았다.
크게 문제는 없지만 코드가 상당히 복잡해 지는 단점이 있었다.
다음 포스트에서는 AsyncTask 객체를 통해 좀더 간단한 코드로 스레드를 구현하는 방법에 대해 살펴 보려고 한다.
'Android > Tips' 카테고리의 다른 글
[Eclipse] Tags 활용법 (TODO, FIXME, XXX....) (0) | 2014.07.24 |
---|---|
JavaDoc 주석 만들기 (0) | 2014.07.24 |
Bitmap to Drawable, Drawable to Bitmap (0) | 2014.07.24 |
Long타입의 파일사이즈를 Formatting 하는 함수 (0) | 2014.07.24 |
Thread 구현하기 2/2 (with AsyncTask & ProgressBar) (0) | 2014.07.24 |