느린 앱은 사용자가 프로그램을 삭제하는 주요 원인입니다. 시작 시 또는 목록 스크롤 시 밀리초 단위의 지연이 리텐션을 수십 퍼센트 감소시킵니다. 성능(performance)은 속도뿐만 아니라 안정성(ANR, 크래시, 메모리 누수 없음)을 의미합니다. 이 글에서는 메모리 관리(GC, ARC)부터 프로파일링 도구까지 성능의 모든 측면을 다룹니다. 자세한 내용은 공식 Android Performance 가이드를 참조하세요.
핵심 요점
앱 성능은 jank(사용자 작업과 UI 응답 사이의 눈에 띄는 지연)와 직접적으로 관련됩니다. 주요 원인: 메인 스레드 차단(UI 스레드에서의 무거운 작업), 빈번한 레이아웃 다시 그리기(overdraw), 메모리 누수(빈번한 GC), 비최적 알고리즘(대규모 데이터에서 O(n²)). 프레임 레이트(FPS) — 초당 프레임 수. 편안한 경험을 위해서는 안정적인 60 FPS(Android) 또는 120 FPS(iPhone Pro, iPad Pro)가 필요합니다. VSync는 화면 재생 빈도와 렌더링을 동기화합니다.
Jank는 단일 프레임 렌더링이 16.6ms(60 FPS의 경우) 또는 8.3ms(120 FPS의 경우)를 초과할 때 발생합니다. GPU 프로파일링(Android의 Profile GPU Rendering, iOS의 Core Animation)은 렌더링의 어떤 단계가 가장 시간을 많이 소비하는지 보여줍니다. 주요 단계: Layout(요소 배치), Draw(그리기), Display(프레임 버퍼로 전송). 가장 일반적인 문제는 XML의 레이아웃 인플레이션, 특히 복잡한 중첩 ConstraintLayout 사용 시입니다.
Time-to-Interactive(TTI) — 앱이 완전히 상호 작용 가능해질 때까지의 시간. TTI에는 콜드 스타트, 데이터 로딩, 라이브러리 초기화가 포함됩니다. Google은 TTI를 5초 미만, Apple은 메인 화면에서 2초 미만으로 권장합니다. 레이지 로딩 — 콘텐츠와 라이브러리의 지연 로딩 기술로, TTI 개선에 중요합니다. IT Sectr에서는 모든 프로젝트에서 기본적으로 지연 초기화를 사용합니다.
ANR과 크래시는 모바일 앱 성능의 주요 적입니다. ANR(Application Not Responding) — 메인 스레드가 5초 이상 차단되면 Android에 나타나는 대화 상자. 원인: UI 스레드에서의 동기 네트워크 요청, 코루틴 없는 데이터베이스 작업, 다운샘플링 없는 큰 비트맵 디코드, 메인 스레드에서의 데드락. ANR 호출 스택은 /data/anr/traces.txt에 저장되며 정확한 차단 위치를 파악할 수 있습니다.
크래시 — 예기치 않은 앱 종료. Android에서는 Exception(Java/Kotlin) 또는 Signal(네이티브 코드). iOS에서는 NSException 또는 신호(EXC_BAD_ACCESS — 해제된 메모리 접근). 크래시 리포팅 도구: Firebase Crashlytics, Sentry, BugSnag. 이들은 스택 추적, 기기 데이터, 재현 단계를 수집합니다. Stack Overflow — 무한 재귀로 인한 호출 스택 오버플로. OutOfMemoryError — 힙이 가득 찬 경우.
StrictMode — 스레드 안전 위반을 감지하는 Android 도구. 규칙 설정 가능: ThreadPolicy(메인 스레드에서 디스크/네트워크 금지), VmPolicy(Activity, SQLite, CloseGuard 누수 감지). StrictMode는 디버그 빌드에서만 활성화하는 것이 좋습니다 — 릴리스에서는 작동하지 않아야 합니다. iOS의 동등한 도구는 Main Thread Checker(Xcode)로, 메인 스레드가 아닌 곳에서의 UIKit 호출을 자동으로 감지합니다.
메모리 누수(Memory Leak) — 앱이 더 이상 사용하지 않는데도 객체가 메모리에 남아 있는 상황. 이는 앱 성능을 직접 저하시킵니다. Android에서 GC(가비지 컬렉션)는 객체에 강한 참조가 있으면 수집할 수 없습니다. 일반적인 원인: Activity에 대한 정적 참조, 지워지지 않은 콜백/관찰자, 외부 클래스에 대한 암시적 참조를 가진 내부 클래스, 지워지지 않은 메시지를 가진 Handler. LeakCanary — 자동 누수 감지 라이브러리.
ARC(Automatic Reference Counting) — iOS의 메모리 관리 모델. 각 객체에는 참조 카운트(retain count)가 있습니다. 카운트가 0이 되면 메모리가 해제됩니다. Retain Cycle은 두 객체가 서로에 대한 강한 참조를 보유할 때 발생합니다(A → B 및 B → A). ARC는 카운터를 절대 0으로 만들지 않습니다. 해결책: 약한 참조(weak) 또는 무소유 참조(unowned). Weak는 객체 해제 시 자동으로 nil이 됩니다. Unowned는 nil이 되지 않지만 객체가 살아있음을 보장합니다.
GC(가비지 컬렉션)는 Android(Java/Kotlin)에서 실행됩니다. GC는 도달할 수 없는 객체를 찾아 해제하기 위해 정기적으로 실행을 일시 중지합니다(Stop-the-World 일시 중지). GC 트리거: 힙이 특정 비율로 채워질 때. ARC는 iOS(Swift/Objective-C)에서 실행되며 일시 중지가 없습니다 — 카운터는 각 할당 시 원자적으로 업데이트됩니다. ARC는 더 예측 가능하지만 높은 할당 빈도에서는 과도한 retain/release 작업이 누적될 수 있습니다.
약한 참조(Weak Reference)와 강한 참조(Strong Reference) — 참조 유형에 따라 GC/ARC가 객체를 해제할 수 있는지 결정됩니다. Strong Reference — 이 참조가 존재하는 한 객체는 수집되지 않습니다. Weak Reference — GC/ARC가 객체를 수집할 수 있음; 약한 참조는 nil이 됩니다(Swift/Java WeakReference). Unowned Reference(Swift) — 해제 시 nil이 되지 않음; 객체 사후 접근 시 크래시 발생. Android에서는 약한 참조를 위해 java.lang.ref.WeakReference를 사용합니다.
LeakCanary를 사용한 Android에서의 누수 감지 예제:
// Утечка: анонимный класс держит ссылку на Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// Используем `this@MainActivity`, сохраняя ссылку на Activity
Log.d("TAG", "Handler received message")
}
}
handler.sendEmptyMessageDelayed(0, 60000)
}
}
// Исправление: статический Handler + WeakReference
class SafeHandler(activity: MainActivity) : Handler() {
private val weakActivity =
WeakReference(activity)
override fun handleMessage(msg: Message) {
weakActivity.get() ?: return
Log.d("TAG", "Handler received message")
}
}
프로파일링은 앱 성능(CPU, 메모리, 네트워크, 전력 소비)을 측정하는 과정입니다. 프로파일링 없는 맹목적 최적화는 무용지물입니다 — 코드의 어떤 부분이 실제로 느린지 알 수 없습니다.
| 도구 | 플랫폼 | 측정 대상 | 사용 시기 |
|---|---|---|---|
| Instruments (Time Profiler) | iOS | CPU, 함수 호출, 실행 시간 | 알고리즘 최적화, 병목 지점 탐색 |
| Instruments (Allocations) | iOS | 메모리, 객체 수, retain counts | 누수 및 과도한 메모리 소비 탐색 |
| Instruments (Leaks) | iOS | Retain cycles, 메모리 누수 | 릴리스 전 정기 점검 |
| Android Profiler (CPU) | Android | CPU 사용량, 스레드 활동, traces | 메인 스레드 차단 탐색 |
| Android Profiler (Memory) | Android | 힙 덤프, 할당 추적 | 누수 탐색, 객체 분석 |
| Android Profiler (Network) | Android | 트래픽, 속도, 요청 시간 | 네트워크 호출 최적화 |
| LeakCanary | Android | 자동 메모리 누수 감지 | 모든 개발 단계 |
| StrictMode | Android | 메인 스레드 디스크/네트워크, 누수 | 디버그 빌드 |
| Traceview / Systrace | Android | 메서드 추적, 시스템 이벤트 | 심층 지연 분석 |
Instruments(Xcode) — iOS를 위한 가장 강력한 도구. Time Profiler는 어떤 함수가 가장 많은 CPU를 소비하는지 보여줍니다. Allocations는 객체 생성 및 해제를 추적합니다. Leaks는 자동으로 retain cycles를 찾습니다. 프로파일링 단계: (1) Instruments 실행; (2) 템플릿 선택(CPU용 Time Profiler); (3) 문제 시나리오 실행; (4) 호출 스택 분석 — 가장 넓은 열이 가장 "핫"한 함수입니다.
Android Profiler는 Android Studio에 내장되어 있습니다(View → Tool Windows → Profiler). CPU Profiler는 각 스레드의 부하를 보여줍니다. Memory Profiler — 힙 덤프 및 할당 추적. Network Profiler — 시간 정보가 포함된 모든 HTTP 요청. Energy Profiler — 전력 소비: WakeLock, Location, Network. 상세 추적을 위해 Systrace(Android 10+) 또는 Perfetto — 마이크로초 정밀도의 시스템 추적을 사용합니다.
앱 시작은 주요 성능 지표 중 하나입니다. 세 가지 유형으로 나뉩니다: 콜드 스타트 — 앱이 처음부터 시작: 프로세스 생성, Application.onCreate(Android) / AppDelegate.applicationDidFinishLaunching(iOS), 클래스 로딩, 라이브러리 초기화. 웜 스타트 — 프로세스는 존재하지만 Activity/ViewController가 소멸됨(예: 화면 회전 또는 메모리에서 복귀 시). 핫 스타트 — Activity/ViewController가 메모리에 있고 앱이 단순히 표시됨(다른 앱에서 전환).
콜드 스타트가 가장 중요한 지표입니다. Android에서 포함: (1) launch Activity — XML 로딩, View 초기화; (2) 첫 번째 프레임 — 첫 렌더링까지 시간. Google 권장: launch Activity < 200ms, 첫 번째 프레임 < 500ms, TTI < 5초. 콜드 스타트 최적화: Application.onCreate 축소(지연 초기화를 위한 코루틴), SplashScreen API(Android 12+) 사용, 라이브러리 초기화 연기(WorkManager, DI), 불필요한 ContentProviders 제거.
iOS에서 콜드 스타트는 다음을 포함: Mach-O 바이너리 로딩, dyld(동적 링커), Objective-C 런타임 초기화, 애플리케이션 델리게이트, 첫 번째 컨트롤러. Chrome Custom Tabs(Android) 및 Universal Links(iOS) — 완전한 콜드 스타트 없이 앱에서 외부 콘텐츠를 빠르게 여는 기술. 중간급 실제 기기에서 콜드 스타트를 테스트하는 것이 좋습니다.
앱 크기는 설치 및 업데이트의 성능 요소입니다. 전환율에 영향을 미칩니다: 10MB마다 전환율이 1% 감소합니다. Google Play 권장 APK 크기 150MB 미만; App Store — 200MB 미만(셀룰러 네트워크 — 100MB). 주요 최적화 방법: 이미지 압축(PNG 대신 WebP 25-35% 절약), 벡터화(Android의 VectorDrawable, iOS의 SF Symbols), 미사용 코드 제거(R8/ProGuard), 미사용 리소스 제거(lint → unused resources).
App Bundle(Android) — Google Play가 각 기기에 최적화된 APK를 생성하는 게시 형식. App Bundle은 다운로드 크기를 20-40% 줄입니다. Dynamic Delivery — 주문형 다운로드 모듈(on-demand feature modules). iOS의 동등한 기능은 On-Demand Resources(ODR): 첫 실행 후 다운로드되는 리소스(게임 레벨, 비디오).
레이지 로딩 — 모듈과 라이브러리가 시작 시 로드되지 않고 필요에 따라 로드되는 기술. Split APK(Android) 및 App Slicing(iOS) — 앱을 아키텍처 슬롯(arm64-v8a, x86_64)으로 분할. 앱 크기 최적화 — 지속적인 프로세스: APK 구성 분석(Android Studio의 Analyze APK), 중복 아이콘 제거, 여러 PNG 밀도 대신 SVG 사용. IT Sectr에서는 모든 MR의 CI/CD에 빌드 크기 검사를 포함합니다.
자주 묻는 질문
ANR(Application Not Responding) — 메인 스레드가 5초 이상 차단되면 Android에 나타나는 대화 상자입니다. ANR을 방지하려면 모든 무거운 작업(네트워크, 데이터베이스, 파일 처리)을 백그라운드 스레드로 이동하세요. iOS의 동등한 현상은 frozen UI로, 앱이 터치에 반응하지 않게 됩니다.
메모리 누수 — 객체에 대한 참조가 남아 있어 해제할 수 없는 상태. Retain Cycle — iOS/Objective-C에서 두 객체가 서로를 참조하는 상황(A → B → A)으로 ARC가 어느 쪽도 해제할 수 없음. 해결책: weak/unowned 참조 및 적시 콜백 정리.
iOS: Instruments(Time Profiler, Allocations, Leaks). Android: Android Profiler(CPU, Memory, Network), LeakCanary(메모리 누수), StrictMode(스레드 위반). 개발 및 통합 중 프로파일링을 결합하는 것이 좋습니다.
콜드 스타트 — 앱이 처음부터 시작: 프로세스 생성, 클래스 로딩, Application.onCreate 실행. 웜 스타트 — 프로세스는 존재하지만 Activity/ViewController가 재생성됨. 핫 스타트 — Activity/ViewController가 이미 메모리에 있고 단순히 표시됨. 콜드 스타트가 가장 느림(1-5초)며 사용자 경험에 중요합니다.
주요 방법: 미사용 리소스 및 코드 제거(R8/ProGuard 사용), 이미지 벡터화(VectorDrawable, SF Symbols), PNG/WebP 압축(Android), APK 대신 App Bundle 사용, 불필요한 라이브러리 제거, 모듈의 레이지 로딩 사용. 크기 최적화는 APK를 40-60%까지 줄일 수 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.