Android/외부활동

[2013.01.19] Handler & AsyncTask - Application 반응 향상

croute 2013. 1. 19. 14:40

Handler & AsyncTask - Application 반응 향상



20130119_Handler_and_AsyncTask_by_croute_blog.pdf

https://developers.google.com/events/29009266/


2013.01.19, 강남 CNN the Biz 교육연구센터에서 있었던, 제 2회 GDG Android Korea 미니 컨퍼런스 발표자료입니다.

발표자료라, 보시는 분들의 이해를 돕기위해 코멘트 관련된 부분을 블로그에 추가합니다.

Q&A 가 조금 있었어서 그 부분도 블로그에 추가합니다.



PDF 를 보고 느끼시지 못할 수 있으시겠지만, 항상 제 프리젠테이션의 컨셉은 재미입니다. ^ㅡ^ ;

재밌고 편한 분위기를 추구하거든요. 아하하;; (청중들이 빵빵 터지도록 만들고 싶어요 항상... ㄷㄷ)









제 2회 GDG Android Korea 미니 컨퍼런스에서 발표했던 내용입니다.

저는 Handler 와 AsyncTask, 애플리케이션 반응 향상을 주제로 잡았습니다.


PPT 를 만들때, 템플릿은 파워포인트 기본 탬플릿으로 하고, 거기에 10인치 태블릿으로 안드로이드의 느낌을... 줬었는데, 발표중에는 아무도 눈치채지 못했던듯 합니다... 으아....









다시 본론으로 돌아와서, 내용은 Handler & AsyncTask, 애플리케이션 반응향상에 대해서구요.

항상 비동기 작업을 처리할때, 어떤걸로 하는게 더 좋을 것인가! 에 대한 고민을 많이 합니다.

Handler 와 Thread 를 사용하는 방법, 그리고 AsyncTask 를 사용하는 방법.

매번 고민하지만, 매번 답이 없기도 한 주제고요.

사실, 개발자가 편하고 컨트롤 하기 쉬운 코드로 작성하는게 하나의 정답이라면 정답이긴 할거에요.


제가 말하는 내용이 정답이라고는 할 수 없지만, 이런 경우 이렇게 처리하는게 어떨까요, 또는 이럴때는 이렇게 처리해봤습니다. 이유는 이렇구요. 라는 식의 의견제시 정도로 생각해주시면 될 것 같습니다.


 







유저들이 애플리케이션이 느리다! 라고 느껴지는 것들을 정리해 봤습니다. (물론 이외에도 많이 있겠지만요.)

서버 통신 중이라면서 화면을 블락할때, 리스트 스크롤이 버벅거릴때, 화면이 멈춰있을 때 등등...

유저는 화면과 관련해 자신이 기대한 결과가 바로 화면에 보여지지 않거나, 자신이 다른 액션을 하는데 있어서 바로 액션을 진행할 수 없을때, 유저는 이 애플리케이션이 느리다! 라고 생각하게 됩니다.


정말 열심히 코딩하고 개발해서 런칭한 서비스가 유저들에게 평가를 잘 받지 못한다면, 너무 속상하겠죠.

어떤 어떤 패턴을 썼고, 여기선 어떤 알고리즘을 썼고, 신기술 어떤걸 써서 정말 잘 만들었는데.... 같은건 유저들에게 쉽게 보여줄 수 있는 것들이 아닙니다. 소스코드를 보여줄 순 없으니까요.

우리는 개발자니까, 개발자가 할 수 있는 방식으로 유저들에게 뭔가를 보여줘야해요.

그 중 한가지가 앱의 반응을 향상시키는 것 이라고 생각합니다.









애플리케이션의 반응을 향상시켜서 유저의 만족도를 올려야 하는데, 

그러기 위해서는 앱이 빨라야 합니다. 안정적이기도 해야겠죠.

여기서 추가적으로, 앱이 빨라야 한다는, 유저가 앱이 빨라졌다고 느끼는 것도 포함합니다.

실제로 더 빨라지지는 않았지만, 유저가 느끼기에 빨라졌다라고 느낀다면, 그건 빠른앱이라는 얘기죠.

같은 코드라도, 화면에 보여지는 것을 어떻게 처리했느냐에 따라 앱이 훨씬 빠르게 느껴질수도 있는거니까요.


대부분의 오래걸리는, 유저가 느리다고 생각하는 작업들은 DB 읽기 쓰기, Network 통신, File 읽기 쓰기, Bitmap 디코딩 같은 작업들입니다.









이렇게 오래 걸리는 작업들은. 비동기로 처리를 해줘야 합니다.






항상 이게 고민이죠. Handler, AsyncTask 중에 뭘 써야 할까요?

어떤 상황에선 Handler 를 쓰고, 어떤 상황에선 AsyncTask 를 써야 할까요?

어마어마한 주제죠. 논쟁이 끊이지 않을만한...









AsyncTask 의 레퍼런스를 읽어보면 이렇게 정리할 수 있습니다.





UI 스레드와 관련해 AsyncTask 가 어떻게 동작하는지 한번 살펴보면, 이렇게 되겠죠.

UI 스레드에서 AsyncTask 를 생성, 실행합니다.

AsyncTask 의 메소드들은 순서대로 호출이 됩니다.

먼저 onPreExecute() 가 실행이 될거고요, (화면에 onPreExecute 에 아무런 표시가 없는데, 여기서도 UI update 를 할 수 있습니다.) 

그 다음, doInBackground() 가 실행이 됩니다. doInBackground 에서는 UI 에 접근할 수 없지만, publishProgress 메소드를 이용함으로써, onProgressUpdate 가 실행되도록 할 수 있으므로, onProgressUpdate 에서 진행 상황을 UI 에 업데이트 할 수 있습니다. 그리고 doInBackground 에서의 모든 처리가 다 끝나고 return 을 해서 메소드를 종료합니다.

그러면 onPostExecute() 가 실행되고, doInBackground 에서 처리된 결과를 가지고(Void 일 수 있음) 결과 처리를 해주고, UI 를 업데이트 해주죠.






UI 스레드에서는 UI 업데이트에 대한 요청을 바로바로 처리해 주지 못할수도 있습니다. 이미 UI 업데이트에 대한 요청이 있다면, 먼저 온 요청을 처리하고 나서 하나씩 하나씩 다음 요청을 처리해주죠. Queue 방식입니다.






간단한 예제 코드 입니다. 여기선 중간중간의 progressUpdate 는 사용하지 않았습니다.

(개인적인 생각일 수 있지만, 화면을 블락하고 작업을 진행하는건 매우 올드하다고 생각합니다. 타이틀바 또는 액션바에 Indeterminate Progress 를 보여주는 방식이 훨씬 깔끔하다고 보거든요. 화면 블락 없는 요즘 방식이라고 생각합니다.)


onPreExecute 에서 타이틀바/액션바에 작업 시작을 알수 있도록 progress 를 보여주고요,

doInBackground 에서 무거운 작업을 진행합니다. 작업이 끝나면 작업 결과(여기서는 Bitmap 객체)를 return 해주고요.

onPostExecute 에서는 넘겨 받은 작업 결과를 처리해줍니다. 여기서는 Bitmap 을 전달받아 이미지뷰에 세팅해주고, 타이틀바/액션바의 Indeterminate progress 를 종료시켜줍니다.


AsyncTask 부분의 코드만 있어 따로 표시는 안되어있지만, 

AsyncTask 가 생성(new) 되고 실행(execute) 된건 UI 스레드입니다.









핸들러는 생성자와 메소드 몇가지만 알아두면 잘 사용할 수 있습니다.


우선 생성자 부터 보도록 하죠.

디폴트 생성자는 new Handler() 를 실행하는 스레드에, 생성된 핸들러의 인스턴스가 바인딩됩니다.

또 Handler 를 생성할때 전달한 Looper 에 바인딩 되도록 할 수 있습니다.

new Handler(Looper.getMainLooper()) 로 핸들러를 생성하면, 어디서 생성했던간에, 생성된 핸들러의 인스턴스는 메인 스레드에 바인딩 됩니다. 메인 스레드는 당연히 UI 스레드죠.


핸들러는 Message 관련되어, Message 를 전달하고 처리하는 메소드 2가지(사실 sendMessage 는 overload 되어있어 훨씬 많지만, 처리 개념상 2가지라고...), 

Runnable 관련되어 직접 Runnable 을 전달하는 메소드 1가지(여기서도 post 관련 메소드는 더 많지만, 처리 개념상 1가지로...) 를 알면 됩니다.


이 두가지 방식을 좀 더 자세히 설명해보죠.





UI 스레드에서 Handler 를 생성하고 post, postDelayed, postAtTime 메소드를 이용해 Runnable 을 MessageQueue 에 전달 할 수 있습니다. 하지만 이 방법은 Runnable 의 run 메소드에서 UI 에 접근할 순 있지만, 사실상 화면이 멈춰있게되죠.

하나의 run 에서 모든 작업이 일어나 버려서 UI 업데이트의 코드는 실행됬지만, 화면에 적용이 되지 않아요. 그래서 유저는 앱이 멈췄다라고 느끼게 되죠. 


여기서는 post 방식 말고, sendMessage & handleMessage 에 대해 더 자세히 알아보도록 할게요.




UI 스레드에서 핸들러를 생성합니다. 이 핸들러는 UI 스레드에 바인딩 되겠죠.

UI 스레드에서 다른 스레드를 하나 생성합니다. 이때 Runnable 을 전달해줘요. Runnable 은 백그라운드에서 처리할 약간은 무거운 작업을 구현해두었습니다.

이제 스레드의 Runnable 의 run 이 실행됩니다. 그리고 핸들러에 sendMessage 를 통해 Message 를 보내죠. 그리고 무거운 작업을 처리하고요. 작업 처리가 끝나면 결과물을 가지고 핸들러에 sendMessage 를 통해 Message 를 보냅니다. 이때 Message 에는 결과를 추가할 수 있죠. 

메시지 큐는 전달 받은 Message 들을 가지고 있다가 Queue 매커니즘에 의해 하나씩 꺼내서, Message 를 처리할 알맞은 핸들러에 전달해줍니다. 핸들러는 메시지 큐가 메시지를 전달해주면, handleMessage 에서 해당 메시지를 처리할 수 있게 됩니다. 

핸들러의 handleMessage 에서는 각각의 case 에 맞게 전달받은 Message 를 처리해주면 되고요. UI 업데이트가 있다면, 그것들도 실행해줍니다. 핸들러는 UI 스레드에 붙어있으므로 UI 업데이트를 할 수 있죠.




그리고 UI 스레드는 UI 업데이트를 queue 매커니즘에 의해 하나식 화면에 반영해줍니다.




간단하게 작성해본 Handler + Thread 방식의 비동기 처리 코드입니다.

핸들러는 화면 처리부분만 맡고 있고요, 실제 백그라운드 비동기 작업은 Thread 에 전달된 Runnable 의 run() 에 구현되어있습니다. run() 에서 sendMessage 를 통해 UI 업데이트 및 결과처리르 할 수 있게 핸들러에 전달을 하구요, 핸들러는 Message 를 받아서 처리를 해주는 구조입니다.


AsyncTask 와 비교해봤을때는, 다소 복잡한 부분이 있다라고 생각될 수 있기는 하지만, 네트워크 통신등 여러가지 처리를 추가하거나 직접 해주고 싶을 때는 훨씬 편리하죠.

예를들어 네트워크 통신 실패시 재시도를 하려고 한다면, AsyncTask 는 new 를 해서 새로 생성해줘야 하지만, Handler + Thread 방식에서는 Runnable 을 재활용하면 되죠.









이제 AsyncTask 와 Handler + Thread 방식에 대해 다시 정리해봅니다.

각각의 장단점 및 권장되는 코드들에 대해서는 이제 조금 알것 같네요.




위에 나온 내용들을 토대로 저는 이렇게 사용을 하고 있습니다.

Network 와 DB 는 Handler + Thread 방식으로, Bitmap 관련된 처리는 AsyncTask 로, File 관련 처리는 AsyncTask 및 Handler + Thread 로 상황마다 조금씩 다르게 처리해주고 있습니다.


Network 관련해서 한가지 주의할게 있는데, UI Thread 에서 직접 network 통신을 시도하는 것입니다.

안드로이드 3.0(허니콤, Honeycomb) 이후 부터는 MainThread 에서 network 통신시, android.os.NetworkOnMainThreadException 이 발생합니다. 


이런 익셉션이나 ANR(Application Not Responding) 관련해 Strict mode 를 참고하면 도움을 받을 수 있습니다.

하위버전(3.0이전)에 맞춰놓고 프로젝트를 진행하다보면 NetworkOnMainThreadException 같은걸 찾기 쉽지 않거든요. 이럴때 Strict mode 가 도움이 됩니다.









페이스북 앱의 화면입니다. 이 화면들을 여기서 어떻게 더 개선할 수 있을지에 대해 얘기를 해보려고 합니다.





1. 첫번째 화면은 올드한 방식의 [화면 블락하는 방법] 보다는 좀 더 나은 방법이라고 생각합니다. 하지만 화면에 아무것도 보여지는게 없죠. 애플리케이션을 처음 실행하는 것이라면 어쩔 수 없지만, 한번이라도 실행한 적이 있다면, 화면에 보여줄 것들을 만들어 놓을 수 있겠죠.


2. Handler + Thread 방식을 사용해, Local DB 에 저장된 것이 있는지 확인하고, 저장되 있는 데이터들이 있다면 불러와서 화면에 보여줍니다.


3. 이때 ListView 에 대한 최적화도 되어있다면 더 빠르겠죠. ViewHolder 를 사용한 Holder 패턴 및 뷰 하이라키를 최소화 하는 방법들을 사용할 수 있습니다.


4. 이제 Handler + Thread 방식을 사용해 Network 통신을 합니다. 글 리스트를 받아오죠. 이렇게 받아온 글 리스트를 화면에 보여줍니다.


5. 4에서 글리스트를 받아와서 화면에 보여줄때, Bitmap 관련된 부분들은 개별적으로 AsyncTask 를 이용해줍니다. 여기서는 프로필 이미지나 첨부 이미지들에 AsyncTask 를 사용할 수 있겠죠. lazy 하게 Bitmap 을 처리해줌으로써 화면의 버벅임 없이 스크롤을 빠르게 할 수 있게 해줍니다.


6. 리스트에서 하나의 글을 선택해서 세부화면으로 넘어갈 때, 이미 [글 데이터]는 가지고 있기 때문에, 글 데이터를 Intent 와 Bundle 을 사용해서 다음 화면으로 넘겨줍니다. 


7. 6에서 넘겨준 데이터를 화면에 세팅해서 보여주고요. 타이틀바/액션바에는 데이터를 받아온다는 표시로 progress 를 시작해줍니다. 여기서 받아올 건 댓글들 리스트죠. 하지만, 글 데이터는 이미 화면에 뿌려져 있으므로, 유저는 빈화면을 볼 필요가 없습니다. 약간은 빨라졌다고 느낄 수 있게되죠.


8~9. 4~5와 같은 방식으로 댓글 리스트를 받아와서 화면에 보여주는데, 각각 Handler + Thread 와 AsyncTask 를 사용해줍니다.


실제 많은 것들이 바뀌진 않겠지만, 조금씩의 변경만으로 유저들은 애플리케이션이 한층 빨라졌다고 느낄 수 있게되죠.


핵심은, 이정도 입니다.

1. 보여줄게 있으면 먼저 보여줘라. 

    (같은 화면을 진입할때마다 네트워크 통신을 새로 하는건 올드한 방법이다. Local DB를 써라.)

2. 화면을 블락하지 말아라. 

    (요즘엔 액션바에 보여주는게 깔끔한 방법이라고 생각한다.)

3. 스크롤을 빠르게 해라. 

    (리스트뷰의 최적화는 이미 모두가 하고 있다고 생각되기는 한다.)

4. 프로필 이미지 등에 대한 비트맵은 lazy 하게 처리해도 된다.


이정도만 해도 많은 리소스의 추가없이 애플리케이션의 반응을 향상시킬 수 있습니다.









관련 링크들입니다. 참고하면 많은 도움을 받을 수 있습니다.


AsyncTask

http://developer.android.com/reference/android/os/AsyncTask.html

http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html

(샘플)http://developer.android.com/shareables/training/BitmapFun.zip - ImageWorker.BitmapWorkerTask class

 

Handler

http://developer.android.com/reference/android/os/Handler.html

http://masl.cis.gvsu.edu/2010/04/05/android-code-sample-asynchronous-http-connections/

http://adnroidandme.wordpress.com/2012/05/08/http-connection-on-android/








질문이 없을 줄 알았는데, 의외로 제 세션에 질문이 몰렸어서, 몇가지 질문들도 함께 포스팅하겠습니다.

당시 질의응답은 발표 후 했던 것이기 때문에 발표 내용에 대한 암묵적인 공통 이해가 있었습니다. 답변당시 말하지 않았던 내용들이라도 발표 중간에 말했던 내용들을 블로그를 보시는 분들을 위해 추가적으로 작성하였습니다.



Q. Handler+Thread 방식이나 AsyncTask 방식, 둘 모두 개발자들이 많이 쓰는 방식인데, 어떤 방식이 더 안정적이라고 생각하는가?


A. 안정성 측면이라면 AsyncTask 라고 생각한다. (+ AsyncTask 는 cancel 메소드를 사용해 태스크의 취소도 쉽고, ) 미리 구현되있는 메소드들에 자신이 원하는 부분의 구현만 추가하면 되기 때문에. Handler + Thread 방식은 각각의 case 를 처리하기 위해 Handler.handleMessage 를 override 해서 각 case 별로 처리를 따로 해줘야 한다. 그리고 Thread 의 Runnable 에서 여러가지 Exception 이 발생할 수 있는데, 이를 run() 에서 예외처리를 해줘야한다. run() 메소드는 자신을 호출한 부분에 Exception 을 전달 할 수 없기때문에, 여러가지 Exception 발생상황에 대해 try-catch 문으로 처리를 해줘야 하고, 예외 발생시 이를 Handler.sendMessage 를 통해 Handler 에 예외발생(에러 발생 등)을 전달해 처리 할 수 있도록 해야 한다. 이런 처리가 제대로 이루어 진다면 상관없겠지만, 아무래도 직접 구현해야 하는 부분들이 많이 있기때문에 안전성에 대해서는 기본적으로 Handler 보다는 AsyncTask 가 더 낫다고 생각한다.




Q. 현재 개발하고 있는 앱이 Bitmap 을 많이 사용한다. 또 이 Bitmap 을 local DB 에 저장해 놓고 불러와서 사용한다. 이럴때 어떻게 처리하는게 좋겠는가?


A. 보통은 Bitmap 을 디스크에 저장하지 않나? DB에 저장해놓고 사용한 프로젝트가 없기는 하지만, 내 경우라면 DB에서 읽어오는(데이터 리스트를) 부분은 Thread + Handler 를 사용해 처리하고, 각각의 Bitmap 을 만들고 처리하는 부분은 AsyncTask 로 처리하겠다.

+ Q&A 당시 시간이 많지 않아, 이유에 대해 자세히 설명하지를 못했다.

각 디바이스에서 DB io 에 대한 속도차이는 고려하지 않더라도, 일반적인 DB 데이터들을 읽고 쓰는건 그렇게 느리지 않고 어느정도 바로 화면에 보여 줄 수 있다, bitmap 을 처리하는걸 Handler 에서 함께 처리한다면, 원하던 원치 않던 화면이 버벅거릴 수 있다. 태스크를 2가지로 나눠서 Handler 에서는 데이터를 읽어와서 화면에 보여주는 부분을 처리하게 하고, AsyncTask 에선 데이터 중 Bitmap 이 있는 경우, 약간은 lazy 하게 처리하도록 한다.




Q. 서버로 대량의 Bitmap 을 전송해야 하는 경우이다. 네트워크 통신에 대해서 Handler + Thread 를 사용하겠다고 했는데 이럴때 어떻게 처리하겠는가? 카카오톡을 보면 한번에 최대 10개의 이미지만을 전달할 수 있다. 어떤 경우를 보면 AsyncTask 가 한번에 10개까지밖에 실행되지 않는 경우가 있는데 이런 이유에서인가?


A. (AsyncTask 를 10개밖에 실행 못한다는건 pass). 

상황에 따라 적절하게 선택해서 처리를 하는게 맞다고 생각한다.(case by case), 일반적인 경우 Network 에서 Handler + Thread 를 사용하는건, 재시도 및 여러가지 선처리, 후처리에 대해 개발자가 여러 처리를 하기 편하기 때문이다.

이 경우 전송을 하면서 progress 를 사용자에게 보여주고 한다면 AsyncTask 를 사용해서 처리하겠다.

AsyncTask 는 처음부터 끝까지 진행하는 하나의 태스크에 대한 진행상태를 보여주기도 쉽고, 단발성 작업들에 최적화 되어있다. 전송할 Bitmap 을 리스트로 만들어서 하나씩 전송하면 될것같다. (재사용을 하지는 못하기때문에, 실패에 대한 처리는 따로 해주어야 한다.) 




다른 질문도 있었는데, 모르는 부분이 있었습니다.

Loader에 관해서 질문하셨던 분이 GDG Android Korea @g+ 에 답변해주신 글 링크

https://plus.google.com/114293542842258089682/posts/KxzjTbnvU8M



질의 중간에 AsyncTask 의 싱글 스레드와 멀티 스레드에 관한 얘기도 살짝 있었습니다.

하나의 앱이라도 버전마다 AsyncTask 가 동작하는 방식이 다를 수 있습니다.

AsyncTask 는 도넛전에 싱글 스레드에서 동작하다가, 도넛이후부터 허니콤전까지 멀티 스레드에서 동작했습니다. 다시 허니콤 이후 싱글스레드에서 동작하도록 변경되었고, AsyncTask 를 멀티 스레드에서 동작하도록 하고 싶다면 executeOrExcutor 메소드를 사용하도록 되어있습니다.

반드시 싱글스레드에서 처리되어야 한다거나, 반드시 멀티스레드에서 처리되어야 한다면, 버전별 분기등을 통해 처리할 수 있습니다. 하지만 멀티 스레드로 AsyncTask 를 처리한다고 해서, 반드시 싱글 스레드에서 처리하는 것 보다 빠르다고 보장되지는 않습니다.




몇가지 질문 및 답변이 더 있었는데, .... 잘 생각이 안나서 이정도에서 마무리할게요~ ^^




아래 사진은 당시 발표실황(?) 사진입니다... ㄷㄷ