본문 바로가기

개발 개발/Android

Android New Gingerbread API: StrictMod

http://www.androidpub.com/1123776


Android New Gingerbread API: StrictMode

[이 포스트는 어플리케이션 반응 속도에 광적으로 집착하는 Brad Fitzpatrick 에 의해 작성되었습니다. - Tim Bray]


배경 이야기

 구글의 장점 중 하나는 바로 '20% 시간' 제도 입니다. 20%의 시간 만큼 여러분이 원하는 프로젝트를 진행할 수 있는 제도이지요. 처음 구글에 입사했을 때, 여기 저기 돌아다니며, 저는 7개의 20% 프로젝트를 진행하고 있다고 농담을 하곤 했습니다. 그리고 그 중 하나가 바로 안드로이드 관련된 일입니다. 저는 안드로이드의 개방성을 사랑합니다. 제가 원하는 것이라면 무엇이든 할 수 있거든요. 심지어, 오토바이를 타고 집에 돌아올 때 쯤 알아서 창고 문을 열어주는 일도 가능합니다. (음...아마도 그런 자작 어플리케이션을 짠 듯?)  안드로이드가 꼭 성공했으면 하는데, 한 가지 아쉬운 점이 있습니다. 안드로이드 플랫폼은 늘 부드럽게 동작하지는 않습니다. 애니매이션은 가끔 뚝뚝 끊기는도 하고, 사용자가 버튼을 눌러도 한 동안 아무 일도 벌어지지 않을 때도 있습니다. 결코 멈춰서서는 안되는 메인 스레드에서 뭔가 잘못된 일이 벌어지고 있음에 틀림 업습니다.

 저는 SMS 를 많이 활용합니다. 그래서, 20% 프로젝트 중 하나로, Cupcake(Android 1.5) 릴리즈 당시 안드로이드 메세징 어플리케이션의 속도를 향상 시키는 일을 하기도 했었지요. 제법 많은 개선이 이루어졌습니다. 그리곤 잠시 다른 일을 하다가, 어느날인가 안드로이드  Donut(Android 1.6)이 릴리즈되고 나서 보니, 이런... 제가 구현한 최적화들이 모두 망가져 있더군요. 우울했습니다. 그리고 생각했조. 보다 근본적으로, 안드로이드 플랫폼에 내장된,  어플리케이션 성능을 모니터링 할 수 있는 방법이 필요하다는 것을 깨닭았습니다.

 그래서 1년 전쯤 안드로이드 팀에 풀타임으로 합류하여, 프로요의 성능 이슈 문제 특히 ANR 관련된 문제를 조사하는데 오랜 시간을 들였습니다. ANR 혹은 어플리케이션의 성능 문제를 디버깅 하는 것은 어렵고 지루한 일이었습니다. 어떻게 원인을 찾아야 하는지 명확하게 설명해놓은 문서도 없더군요. 특히나, 여러 프로세스가 연관된 문제(Binder, ContentResolver, Service, ContentProvider 등과 연관된)는 최악입니다. 무언가 좀 더 나은 방법이 필요했습니다.

여러분은 지금 StrictMode 에 진입하였습니다.

"당신은 지금 16 ms 제한 구역에서 120 ms 로 달리고 있습니다..."

 StrictMode 는 진저브레드에서 추가된 새로운 API 입니다. 여러분이 특정 스레드에서 일어날 수 없는 일들을 미리 규약(Policy)으로 지정한 후, 만일 그런 사태가 벌어졌을 때 어떤 처리(Penalty)를 할지 결정할 수 있겠도와줍니다. 각각의 규약은 개별 스레드의 로컬 정수형 변수의 비트마스크 형식으로 구현되어있습니다. 기본적으로 스레드에서는 무슨일이든 허용됩니다. 그리고 여러분은 아래의 플래그들을 이용해서 하나씩 하나씩 규약 조건을 추가할 수 있습니다.

  • 규약: detect disk writes - 디스크 쓰기 작업
  • 규약: detect disk reads - 디스크 읽기 작업
  • 규약: detect network usage - 네트워크 사용
  • 위반 시: log - 로그를 남기기
  • 위반 시: crash - 강제 종료시키기
  • 위반 시: dropbox - dropbox 에 기록하기
  • 위반 시: show an annoying dialog - 다이얼로그 박스 띄우기

 이런 저런 조건이 설정 되고 나면, StrictMode 는 해당 스레드에서 디스크 I/O 작업이 일어나는 경우 (java.io.*, android.database.sqlite.* 등) 혹은 네트워크 작업 (java.net.*) 이 일어나는 경우, Policy 를 확인 하고 적절한 조치를 취하게 됩니다.

 StrictMode 의 또 하나의 중요한 특징은 바로 는 특정 Policy 가 적용된 스레드 내부에서 Binder IPC 를 통해 다른 스레드 혹은 다른 프로세스의 메서드를 사용하는 경우에도 동일한 Policy 가 적용된다는 점 입니다. 예를 들어, 디스크 읽기가 금지된 스레드 내에서 외부 ContentProvider 를 통해 디스크를 읽어 들이는 경우에도 해당 사실이 탐지되며, 이와 관련된 Stack Trace 정보가 전달됩니다. 이는 한 스레드에 적용된 Policy 가 이 스레드에서 접근 하는 다른 스레드에도 전파될 수 있도록 구현되어있기 때문입니다.

느릿 느릿 움직이는 어플리케이션을 좋아하는 사람은 아무도 없습니다.

 여러분은 직접 작성하신 어플리케이션의 어떤 곳에서 디스크 I/O 작업을 수행하는지는 전부 알고 계실 수도 있습니다. 하지만 사용하고 있는 시스템 서비스와 프로바이더가 디스크 I/O 작업을 하고 있는 부분은 어떨까요? 저는 자신 없습니다. 살펴봐야할 내용이 너무 많더군요. 물론, 안드로이드 개발팀은 성능에 문제를 일으킬 수 있는 API 가 있는 경우 해당 내용을 SDK 문서에 명시하고 있습니다. 하지만, 이제는 SDK 문서를 샅샅히 뒤지는 대신 무심코 메인 스레드에서 I/O 작업을 수행하는 부분을 찾아내기 위하여 StrictMode 를 사용하고 있습니다.

휴대폰 메모리에 관하여 

 그런데 잠깐, "디스크 I/O 작업을 하는게 뭐가 문제인거조? 안드로이드 디바이스는 모두 플레쉬 메모리를 갖고 있잖아요? 그거 엄청 빠른 SSD 같은거 아닌가요? 디스크 I/O 작업이 좀 있다 한 들 별로 상관 없을거 같은데..." 라고 생각하시는 분들이 있을지도 모르겠네요. 하지만 불행히도 이는 사실이 아닙니다.

 여러분은 안드로이드 디바이스에서 사용되는 플래쉬 메모리 혹은 파일 시스템이 늘 빠를 거라고 가정해서는 안됩니다. 예를 들어,현재 많은 안드로이드 디바이스에서 사용하고 있는 YAFFS(Yet Another Flash File System) 파일 시스템은 I/O 작업을 한 때 Global 범위의 락이 사용됩니다. 즉, 전체 디바이스 상에서 오직 한 번에 하나의 디스크 작업만이 가능한 것이지요. 따라서, 운이 나쁘다면 아주 단순히 'stat' 메서드를 사용하는 데에도 제법 긴 시간이 걸릴 수 있습니다. 전통적인 Block 기반의 파일 시스템을 사용하는 다른 디바이스라 하더라도, 파편화된 블럭을 모으기 위한 GC 작업이 수행되거나 플래쉬 메모리에서는 매우 오랜 시간이 걸리는 삭제 작업이 이루어지고 있는 중이라면 I/O 요청을 수행하는데 오랜 시간이 걸릴 수 있습니다. (보다 자세한 배경 지식이 알고 싶으시다면 lwn.net/Articles/353411 를 참조하세요.) 모바일 디바이스에서 디스크 (혹은 파일 시스템) I/O 작업의 90% 정도는 생각보다 느립니다. 더군다나 메모리 여유 공간이 적어지면 적어질 수록 더더욱 느려지지요. (다음 슬라이드를 확인해 보세요. Google I/O Zippy Android apps talk)

메인 스레드

 안드로이드 콜백과 라이프 사이클 관련된 이벤트들은 모두 메인 스레드(UI 스레드)에서 처리됩니다. 다시 말해, 모든 애니매이션, 스크롤 그리고 플리핑 프로세스들도 전부 이 메인 스레드에서 콜백 형식으로 처리 됩니다. 따라서, 예를 들어 여러분이 60fps 로 동작하는 애니매이션을 진행하면서 동시에 사용자의 입력을 처리하고자 한다면, 여러분이 메인 스레드 상에서 사용자의 입력에 반응 하는데 사용할 수 있는 시간은 오직 16ms 뿐입니다. 만일 I/O 작업 혹은 다른 이유로 인해 이보다 오랜 시간이 걸릴 경우, 애니매이션이 버벅되기 시작 할 것 입니다. 물론 일반적인 경우, 단순한 디스크 읽기 작업에 16ms 의 시간이 걸리지는 않겠지만, 상황에 따라 꼭 그런 것 만도 아닙니다. YAFFS 파일 시스템의 경우라면 다른 프로세스에서 디스크 쓰기 작업을 위해 락을 잡고 있는 경우 그런 일이 빈번하게 발생 할 수 있습니다.

 네트워크는 특히나 더욱 느리고 예측 불가합니다. 따라서, 메인스레드에서는 절대로 네트워크 작업을 해서는 안됩니다. 사실, 앞으로 공개될 허니컴에서는 메인스레드에서 네트워크 작업이 일어나는 경우 이를 치명적인 에러로 인지하고 프로그램을 종료시켜버릴 예정입니다. 다시 한번 강조하지만, 만일 여러분의 어플리케이션이 허니컴 이 후 버전에서도 잘 동작하길 원하신다면, 절대로 UI 스레드 상에서 네트워크 작업을 해서는 안됩니다.

StrictMode 사용하기

 추천되는 방식은 다음과 같습니다. 여러분이 어플리케이션을 개발하는 동안에는 StrictMode 를 켜 두세요. 발생하는 여러가지 오류를 수정하고 어플리케이션 성능을 향상시키시기 바랍니다. 그리고 마지막에 어플리케이션을 릴리즈 시에는 이 기능을 비활성화 시켜 두시면 됩니다.

예를 들어 여러분의 어플리케이션의 onCreate() 시점에 다음과 같은 내용을 추가 하실 수 있습니다.
 public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()
                 .penaltyLog()
                 .build());
     }
     super.onCreate();
 }
 혹은 간단히,
    public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.enableDefaults();
     }
     super.onCreate();
 }
 이렇게 하실 수도 있습니다. 아래쪽의 간단한 메서드 StrictMode.enableDefaults() 는 개발자 분들이 진저브레드 이전 버전을 타켓으로 어플리케이션을 개발할 때도 손쉽게, reflection 등을 이용하여, StrictMode 를 사용할 수 있도록 돕기위해 추가되었습니다. 예를 들어, 여러분의 1.6 도넛 타켓 어플리케이션을 개발하면서, 진저 브레드 에뮬레이터 혹은 디바이스 상에서 Strict Mode 를 이용하여 해당 어플리케이션을 테스트 할 수 있습니다. Reflection 을 이용하여 StrictMode.enableDefaults() 만 호출 하시면 말이지요.

StrictMode 결과를 살펴보기

 만일 기본 모드인 penaltyLog() 를 사용한다면, 특정 규약을 위반하는 경우가 발생하는 순간 해당 내용이 로그캣 콘솔에 출력됩니다. 만일 좀 더 멋진 결과를 원하신다면, penaltyDropbox() 를 사용하세요. 이 경우 규약 위반 내용이 DropBoxManager 에 쓰여지며, 이 후에 필요할 때, adb shell dumpsys sropbox data_app_strictmode --print 명령을 통해 해당 내용을 확인 하실 수 있습니다.

어플리케이션을 부드럽게 만들기 위한 팁들

 스레드와 java.util.concurrent.* 패키지를 활용하세요. 그리고 Handler, AsyncTask, AsyncQueryHandler, IntentService 등의 안드로이드 API 를 활용하시기 바랍니다.

구글 팀도 StrictMode 를 유용하게 사용하였습니다.

 안드로이드를 개발하면서 우리는 매일 매일 내부 개발자 버전 ("dogfood" 빌드)을 릴리즈하여 직접 사용해봅니다. 그리고 진저 브레드 버전을 개발하는 동안에는 StrictMode 로깅기능을 켜놓고 모든 로그를 출력해 보았습니다. 매시간 MapReduce Job 이 돌면서, 엄청난 양의 로그를 정리하고 의미있는 리포트를 출력했지요. 이벤트 루프가 멈칫 거리는 경우의 Stack Trace 정보와, Latency 퍼센트, 어떤 프로세스 혹은 패키지가 문제를 일으키는가 등등...

 StrictMode 를 사용해서 우리는 플랫폼 전체적으로 응답성을 저해하고 애니매이션에 문제를 일으킬 수 있는 수백개에 달하는 버그를 수정하였습니다. 특정 어플리케이션에 관련된 문제들 뿐만 아니라, 시스템 코어 쪽에도 성능 최적화가 이루어졌습니다. 만일 여러분이 지금 프로요 버전을 사용하고 있더라도, 최신 GMail, Google Maps, YouTube 등의 어플리케이션을 업데이트 받으신다면 StrictMode 를 통해 개선된 성능을 직접 체험하실 수 있을 것 입니다.

 시스템 내부적으로 성능을 개선하는데 한계가 있는 몇몇 경우에는 새로운 API 를 추가하기도 하였습니다. 예를 들어, SharedPreferences.Editor.apply() 메서드가 추가되었습니다. 이 메서드는 기존의 commit() 대신에 사용하실 수있습니다. 만일 여러분이 commint() 메서드의 리턴 값을 확인 할 필요가 없다면. (사실 이 값을 확인 하는 사람은 아무도 없더군요.) 물론 여러분은 reflection 을 통하여 사용자 플랫폼에 따라 apply() 혹은 commit 가 호출 되도록 구현할 수도 있습니다.

 프로요를 사용하다가 진저버드로 갈아타신 분들은 시스템의 반응성이 굉장히 좋아진 것을 보고 깜짝 놀라실 겁니다. 우리 이웃의 크롬 팀도 최근 StrictMode 와 유사한 기능을 추가하였습니다. 물론 성능 향상이전적으로 StrictMode 의 덕분은 아닙니다. 새롭게 추가된 Concurrent GC 도 큰 역할을 하였습니다.

향후 계획

 StrictMode API 는 계속 확장될 것 입니다. 허니컴에 추가될 몇 몇 멋진 기능들은 이미 계획되어있습니다. 그 외에 여러분이 특별히 필요하다고 생각되는 기능이 있으면 언제든지 알려주세요. 우리는 stackoverflow.com 에서 “strictmode” 라고 태그가 붙은 질문들을 늘 모니터링 하고 있습니다.

감사합니다.