모바일 앱 개발 최적화 노하우
학생의 신분으로 몇몇 안드로이드 환경 프로젝트를 진행하면서 애플리케이션의 크기가 커질 경우 큰 어려움을 겪곤 했다. 애플리케이션 개발 프로젝트가 조금만 커지더라도 최적화하는 데 많은 고민이 요구됐기 때문이다. 그렇다면 어떤 점들이 문제였고, 그 해결 방법은 무엇이었을까? 이 글에서는 안드로이드 기반 모바일 앱 개발 과정에서 경험을 통해 얻은 생각과 노하우를 모아 소개한다.
이원희 maestro.wh@gmail.com|중앙대학교 컴퓨터공학부 4학년에 재학 중인 초보 학생 개발자다. 소프트웨어 마에스트로 2기 연수생으로 활동했으며, 현재는 삼성소프트웨어멤버십 강남지역 정회원이다. SE, 모바일 플랫폼, 병렬처리에 관심이 많으며 S펜 SDK를 활용한 프로젝트를 진행하고 있다.
변광민 yelllowsun@gmail.com|세종대학교 전자정보통신공학부 4학년에 재학 중인 학생 개발자로 현재 삼성소프트웨어멤버십 강남지역 정회원으로 활동하고 있다. 모바일 플랫폼, 병렬처리, 신호처리에 관심이 많으며 S펜 SDK를 활용한 프로젝트를 진행 중이다.
신정수 hello_mile2@naver.com|덕성여자대학교 인터넷정보공학과 4학년에 재학 중인 학생 개발자다. 현재 삼성소프트웨어멤버십 강북지역에서 정회원으로 활동 중이며, 안드로이드 모바일 플랫폼을 주 관심 분야로 공부하고 있다. 최근 안드로이드 기반의 센서 프로그래밍 관련 프로젝트를 진행했다.
프로그램이 정확한 결과를 내는 것은 중요하다. 하지만 정확한 결과를 내는 것이 보장된다면 이 결과를 내는 과정(속도나 다른 측면 등) 또한 중요할 것이다. 따라서 프로젝트 수행 시, 결과물을 내는 과정에서 최적화는 매우 중요한 이슈라고 할 수 있다.
필자의 경우 아직 남들보다 많은 것을 경험해 보지 못했지만 프로젝트의 결과물은 이 결과물을 사용하는 사람들과 소통할 수 있는 또 다른 나라고 생각한다. 또한 그 결과물이 좀더 많은 사용자들에게 이용됐으면 하고, 또 좋은 의미로 다가갔으면 좋겠다고 생각하며 나름대로 고민하고 있다. 다음의 항목들은 이 고민들을 Accessibility와 Efficiency 측면에서 일부 다뤘다.
Multi Screens & Density
현재 안드로이드는 다양한 해상도와 density를 지원하는 여러 기기에서 동작하고 있다. 기본적으로 각각의 기기에 맞게 내부적으로 조절하지만, 이것을 최적화하는 것은 개발자의 몫이다. 필자의 경험으로는 처음에 개발을 시작할 경우 타겟 디바이스를 잡고 layout을 작성할 때 px와 AbsoluteLayout을 지양할 경우 대부분 비슷한 해상도의 다른 기기에서 별 무리 없이 적용이 가능했지만, 차이가 커질 경우(예 : 비 태블릿과 태블릿의 경우)에는 별도의 처리가 필요했었다.
<그림 1> density 분류에 따른 drawable의 상대적 크기(http://developer.android.com)
안드로이드의 경우 이 문제를 layout 파일과 drawable 파일들을 옵션에 따라 분류해 해결하고 있다. 애플리케이션 구동 시에 안드로이드 시스템은 각 density에 알맞은 리소스를 선택하거나 이미 존재하고 있는 리소스를 불러온 후 이것을 조절해 각 디바이스마다 다른 density 문제를 해결한다(Android developer 사이트에서 제공하는 drawable과 layout 관련 설명을 보면 이에 대해 자세히 알 수 있다).
3.2 이전 버전에서는 보통 Screen size를 small, normal, large, xlarge로 구분해 layout 파일을 알맞은 폴더에 배치하면, 안드로이드 시스템은 기기의 Screen에 맞게 알맞은 layout 리소스를 선택한다. 이때 layout 폴더 안에 들어가는 layout 파일의 이름을 같게 하고, 배치되는 view의 id를 같게 할 경우 코드상에서는 단순히 하나의 layout 파일을 보는 것과 같은 모습을 보여준다. 또한 이 폴더명에 land, port를 붙여 Screen orientation에 따른 layout도 세세하게 설정할 수 있다. 이 경우 프로그래머는 layout 파일을 수정해 여러 설정에 맞는 layout 파일을 만들고 디자이너는 drawable 리소스를 각각 만들어 여러 상황에서의 Multi Screens 환경을 구현할 수 있다.
<그림 2> 갤럭시 노트 10.1과 노트1에서 사용한 resource folder
하지만 이 방법을 사용할 경우 7인치 태블릿의 경우 처리가 곤란하다. 7인치 태블릿은 large 영역에 위치하므로 5인치 정도의 기기와 동일한 영역에 위치하지만 사실상 태블릿으로서의 layout을 작성해야 한다. 따라서 안드로이드는 추가로 3.2 버전이 나오면서 좀더 세세하게 Screen size를 나눌 수 있는 방법을 제시했다. dp를 활용해 그룹을 좀더 세밀하게 나누는 방식으로서 smallestWidth, Available screen width, Available screen height의 방법으로 설정할 수 있다.
Nine patch
애플리케이션을 개발하다 보면 UI 작업이 많은 프로젝트일 경우 리소스 파일이 커서 애플리케이션이 전체적으로 무거워지는 상황이 발생할 수 있다. 또한 이미지 형태를 동적으로 변화시켜야만 할 수도 있으며 이때 Nine patch는 대안이 될 수 있다.
Nine patch는 일반 png 파일의 가장자리 부분에 1픽셀씩 추가해 늘어나는 영역을 지정할 수 있다. 확장자는 .9.png가 되며 일반적으로 버튼이나 에디트 텍스트 창에서 많이 사용된다.
<그림 3> Android SDK Tool의 drow9patch 예
<그림 3>은 Android SDK Tools의 draw9patch를 실행해 간단한 Nine patch를 그려본 모습이다. 원본 이미지에서 가장자리 1픽셀이 추가돼 (width+2) × (height+2)의 크기가 된다. 왼쪽과 위쪽의 검은 선은 늘어날영역을 정하는 영역이며 오른쪽과 아래쪽 영역은 패딩 영역을 결정한다.
Nine patch를 사용할 경우 이미지 용량이 확실히 줄어들게 되고, 여러 사이즈의 이미지를 만들 필요도 없어서 적절한 리소스 크기를 유지할 수 있다.
List 최적화
2010년에 구글 IO(Google IO)에서 소개된 Holder Pattern을 적용한 List에 대해 알아본다. 안드로이드의 ListView는 getView를 통해 리스트의 행별 레이아웃을 처리한다. 이때 문제는 getView의 지속된 호출로 인한 성능 저하다. 안드로이드는 리스트의 아이템들을 재활용하며 getView를 지속적으로 호출하기 때문에 아무 처리 없는 getView의 호출은 리스트의 속도 저하로 이어진다.
첫 번째 해결책으로 getView의 인자로 넘어오는 convertView를 사용하는 방법이 있다. 이를 통해 inflate되는 비용을 최소화할 수 있으며 convertView가 null일 경우 inflate한다.
<리스트 1> 대표적인 Screen 크기에 대한 dp 분류표 320×480 mdpi, 480×800 hdpi 등) : 7인치 태블릿 (600×1024 mdpi) |
두 번째 성능 향상 방법은 위의 방법에서 Holder pattern을 사용하는 것이다. findViewById는 많은 자원을 사용하는 함수이므로 잦은 호출을 방지해야 한다. 이때 Holderpattern을 사용한다.
<리스트 2>convertView를 사용한 getView parent) { inflater.inflate(layout, parent, false); |
기본적으로 안드로이드의 View는 setTag, getTag를 사용해 원하는 객체를 저장하고 꺼내 쓸 수 있다. 따라서 setTag, getTag를 통해 저장할 객체를 홀더라고 해서 새로 정의하고, 홀더 안에 내부 뷰에 대한 참조를 보관해 findViewById의 잦은 호출을 막을 수 있다. 이때 convertView가 null일 경우 findViewById를 통해 리턴된 위젯을 홀더 안에 멤버로 넣는다.
<그림 4> 구글 IO 2010에서 소개된 getView를 통한 ListView 성능 향상 비교
추가적으로 ListView의 경우 페이지가 넘어갈 경우 가려진 행의 아이템을 재사용하기 때문에 convertView가 null이 아닌 값을 받아와도 이것에 새로운 아이템을 set해야 된다. 하지만 getView의 경우 바꾸지 않아도 될 view를 부르는 경우도 있어서 필자는 Holderpattern에 더해서 adapter의 아이템 리스트에 그 아이템의 참조를 홀더의 멤버 변수로 할당한 후에 객체 비교를 통해 같은 객체인지 여부를 확인한다. 이때 비교하는 객체가 다를 경우에 새로운 내용을 뷰에 set한다. 이것은 각 행의 layout이 복잡하거나 이미지가 들어가 있을 경우에 유용하며 각 행의 layout이 일반적일 경우 <리스트 3>과 같은 처리만으로도 큰 성능 향상을 가져올 수 있다.
<리스트 3> 구글 IO 2010에 소개된 Holder pattern 적용 getView 예제 parent) { if (convertView == null) { mInflater.inflate(R.layout.list_item_icon_text, parent, false); ViewHolder(); holder.icon = (ImageView) convertView.findViewById(R.id.icon); (holder); holder.text.setText(DATA[position]); |
Lazy image loading 위의 ListView의 최적화 방향 중 하나가 될 수 있는 방법이다. ImageView에서 이미지를 불러올 때 이 속도가 스크롤 또는 다른 UI 작업에 영향을 최소화해야 되며 이때 Lazy image loading 기법이 사용된다(물론 WebView를 통해 이미지를 보여줄 수 있지만, 커스터마이징의 한계점이 많다).
<그림 5> GalleryView를 사용한 이미지 처리 예
만약 불러야 할 이미지가 많지 않다면 Lazy image loading 처리를 하지 않는 것이 더 좋을 수도 있다. 하지만 서버와 통신하는 상황에서 어떤 아이템들의 리스트를 받아와(이미지의 URL을 포함해) 화면에 표시해야 할 때 대부분의 경우 많은 양의 데이터를 표시해야 하며, 이때 URL에서 이미지를 Bitmap으로 받아와 ImageView에 표현하게 된다. 이때 이미지를 받아오는 시간이 오래 걸려서 사용자와 애플리케이션 간의 인터렉션을 저해하는 상황이 발생할 수 있으며 Lazy image loading 기법을 사용해 이것을 해결할 수 있다(자세한 내용은 Android Developers Blog의 Gilles Debunne가 작성한 Multithreading For Performance 포스트를 참고하길 바란다).
Lazy image loading 기법을 사용하기 위해서는 Multi-threading 기법을 사용해야 한다. 보통 AsyncTask를 사용해 처리한다(다른 스레드 처리 방법 또한 개발자의 취향대로 사용할 수 있다). 이때 중요한 점은 동시성 문제다. ImageView의 참조를 갖고 있는 스레드의 경우 재활용하는 ListView에서 같은 참조를 갖고 있는 스레드가 2개 이상 실행될 수 있다. 이 경우 ImageView에서 어느 이미지가 보일지 보장하지 못하며, 이 처리를 하는 루틴이 필요하다.
<리스트 4> HashMap 생성 예HashMap<String, SoftReference<Bitmap>> cache = |
Android Developers Blog에 포스팅된 글에서는 각각의 ImageView가 마지막으로 다운로드한 작업을 기억하게 하면서 이 동시성 문제를 해결했다. 또한 사용자와의 인터렉션이 빈번한 상황에서 이전에 다운로드했던 이미지를 다시 받아야 하는 경우 가 생긴다. 이 경우 URL과 Bitmap 객체를 인자로 하는 해시맵을 사용해 캐싱함으로써 오버로드를 줄일 수 있다.
ImageLoader 소개
Lazy image loading을 직접 구현할 수 있지만, 필자의 경우 간단한 프로젝트나 라이선스에 별 문제가 생기지 않을 경우에 위의 ImageLoader를 사용해 왔다. 2010년도에 사용했을 경우에는 동시성 문제가 있어서 소스를 수정해야 하는 경우가 있었지만, 현재의 경우 매우 높은 완성도를 보여주고 있으므로 이를 사용하면 애플리케이션 개발 기간을 줄일 수 있을 것이다(물론 이런 모듈은 자신이 직접 개발할 경우 계 속 재사용할 수 있다).
<그림 6> imageLoader 홈페이지(http://androidimageloader.com/)
ImageLoder는 메모리 캐싱과 파일 캐싱을 지원하며, 이에 관련된 세세한 사항들을 옵션을 통해 설정할 수 있다. 또한 동시성을 지원하며 오픈소스이기 때문에 사용자가 입맛에 맞게 소스 코드를 수정해 사용할 수 있다. ImageLoader는 싱글톤 패턴을 사용해 관리되며 display Image 시에 내부의 ImageLoadingListener를 등록할 수 있어서 이미지 로딩 시의 다양한 처리를 할 수 있다. 또한 라이선스가 Apache 2.0이기 때문에 소스 코드 공개 등의 의무에 대한 걱정을 줄일 수 있다.
<리스트 5> ImageLoader option 설정 예 (mContext). threadPoolSize(3) .denyCacheImageMultipleSizesInMemory() enableLogging().build(); |
<리스트 6> ImageLoader 사용 예 DisplayImageOptions.Builder().cacheInMemory(). cacheOnDisc().build(); */, /* ImageView */, options); ImageLoadingListener */); |
Layout 구조의 최적화
Layout의 경우 초기화, 배치, 화면 드로잉이 필요하다. 이때 LinearLayout을 잘못 사용했을 경우에 최적화되지 못한 계층화된 layout 구조를 얻을 수 있다. 보통 RelativeLayout을 사용하면 두 계층으로 끝낼 수 있지만, LinearLayout을 겹으로 쌓아서 문제가 발생한다. 이 문제를 Android SDK Tools의 Hierarchy Viewer를 통해 확인할 수 있다.
<그림 7> Hierarchy Viewer 예시
<그림 7>처럼 Hierarchy Viewer를 통해 Measure, Layout, Draw 항목으로 성능을 비교해볼 수 있으며 layout의 구조를 쉽게 파악할 수 있다. 여러 layout을 겹친 List 등의 상황에서 layout 최적화는 매우 중요하며 위의 도구를 사용해 최적화하는 데 도움을 받을 수 있다.
Android SDK Tools의 메모리 분석 도구
안드로이드의 DDMS는 Allocation Tracker와 VM Heap이라는 유용한 메모리 분석 툴을 제공한다. 간단한 애플리케이션에서는 가비지 컬렉션을 믿는 것이 편리하겠지만, 규모가 큰 애플리케이션 개발에서 메모리 분석은 필수다. 큰 오브젝트의 참조가 사라지지 않은 상황에서 가비지 컬렉션은 이 오브젝트의 메모리를 해제하지 않기 때문에 만약 오브젝트가 필요하지 않게 된 상황에서 가비지 컬렉션이 작동하지 않는다면 문제가 있는 것이다.
<그림 8> 안드로이드 DBMS의 VM Heap
<그림 9> 안드로이드 DBMS Allocation Tracker
VM Heap의 경우 Show Heap Update를 활성화시키고 VM Heap 탭을 통해 확인할 수 있다. VM Heap은 GC를 동작시킬 수 있으며 이것을 통해 메모리 누수가 일어나는지 대략적으로 확인할 수 있다. Show Heap Update 버튼 오른쪽에 있는 Dump HPROF file을 통해 HPROF 포맷의 덤프파일을 얻을 수 있고, 이 덤프파일은 Eclipse Memory Analyzer(MAT)를 통해 좀더 자세한 정보를 확인할 수 있다.
<그림 10> Allocation Tracker의 한 객체에 대한 상세 정보
Allocation Tracker는 상단의 StartTracking 버튼을 누른 이후부터 할당된 객체의 정보를 볼 수 있다. 이 객체를 클릭할 경우 객체의 타입과 어떤 Thread에서의 생성인지 또는 어떤 Class, 어느 파일의 몇 번째 줄에서의 생성인지를 알 수 있다. 이 Allocation Tracker를 통해 객체의 빈번한 생성이나 생성되지 말아야 할 곳 등에서의 생성을 파악할 수 있어서 개발자가 실수를 수정하는 데 도움을 준다.
정리하며
지금까지 초보 개발자로서 여러 안드로이드 프로젝트를 경험하며 느낀 것과 얻은 점을 다뤘다. 학생의 신분에서 쌓은 경험과 지식이라 다소 부족할 수도 있지만, 필자와 비슷한 입장에서 향후 안드로이드 개발을 시작하는 분들에게 도움이 되었으면 하는 바람이다. 마지막으로 이 글에 대한 추가적인 궁금점과 피드백 사항이 있다면 필자의 이메일(maestro.wh@gmail.com)로 문의하길 바란다.
참고자료
1. http://developer.android.com
2. http://dl.google.com/googleio/
본 블로그는 페이스북 댓글을 지원합니다.