안드로이드 Activity 자주 발생하는 오류와 해결책 및 성능 최적화 팁
안녕하세요. Android 개발의 기본, Activity 완벽 이해하기 (초보자 가이드) 1편에 이어 2편을 연재하도록 하겠습니다. 2편은 1편보다 심화된 내용으로 정리해 보도록 하겠습니다.
1편은 아래 링크에서 다시 보실 수 있습니다.
Android 개발의 기본, Activity 완벽 이해하기 (초보자 가이드) 1편 (ttps://eunplay.tistory.com/169)

안드로이드 Activity 오류 해결 및 성능 최적화 완벽 가이드
안드로이드 앱을 개발하다 보면 Activity 관련 오류를 자주 마주치게 됩니다. 이런 오류는 앱 충돌이나 성능 저하로 이어질 수 있죠. 이 글에서는 안드로이드 Activity 개발 시 자주 발생하는 오류와 해결책, 그리고 성능을 최적화하는 방법을 쉽고 자세하게 알아보겠습니다.
목차
#10. 자주 발생하는 Activity 오류와 해결책
1. 메모리 누수(Memory Leak)
메모리 누수는 안드로이드 앱 개발에서 가장 흔히 발생하는 문제 중 하나입니다. 특히 Activity 컨텍스트를 잘못 사용할 때 자주 발생합니다.
주요 원인:
- Activity 컨텍스트를 정적(static) 변수에 저장
- 내부 클래스에서 암시적으로 Activity 참조 유지
- 비동기 작업에서 Activity 참조를 오래 유지
해결책:
① Application 컨텍스트 사용하기
긴 수명주기가 필요한 객체에는 Activity 컨텍스트 대신 Application 컨텍스트를 사용합니다.
// 잘못된 방법 ❌
val context = this // Activity 컨텍스트
// 올바른 방법 ✅
val context = applicationContext
② 약한 참조(WeakReference) 사용하기
비동기 작업에서는 Activity에 대한 강한 참조 대신 약한 참조를 사용합니다.
class MyAsyncTask(activity: MainActivity) {
// 약한 참조로 Activity 저장
private val weakActivity = WeakReference(activity)
fun doWork() {
// 작업 수행 후 UI 업데이트
val activity = weakActivity.get()
if (activity != null && !activity.isFinishing) {
// 안전하게 UI 작업 수행
}
}
}
③ 생명주기 인식 컴포넌트 사용하기
ViewModel, LiveData, Coroutines와 LifecycleScope 등 생명주기를 인식하는 컴포넌트를 활용합니다.
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 생명주기에 맞게 자동으로 정리됨
lifecycleScope.launch {
// 비동기 작업 수행
}
}
}
2. 화면 회전 시 데이터 손실
안드로이드 기기를 회전하면 Activity가 재생성되면서 일시적인 데이터가 손실될 수 있습니다.
주요 원인:
- 화면 회전 시 Activity 재생성
- 임시 데이터를 로컬 변수에만 저장
- 구성 변경 이벤트 처리 부재
해결책:
① ViewModel 사용하기
ViewModel은 화면 회전과 같은 구성 변경에도 데이터를 유지합니다.
// ViewModel 정의
class MainViewModel : ViewModel() {
var userName: String = ""
var userScore: Int = 0
}
// Activity에서 사용
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// ViewModel 초기화
viewModel = ViewModelProvider(this).get(MainViewModel::class.java)
// 데이터 표시 또는 변경
binding.textUserName.text = viewModel.userName
}
}
② onSaveInstanceState() 활용하기
Bundle을 통해 데이터를 저장하고 복원할 수 있습니다.
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// 데이터 저장
outState.putString("user_name", userName)
outState.putInt("score", score)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 저장된 데이터 복원
if (savedInstanceState != null) {
userName = savedInstanceState.getString("user_name", "")
score = savedInstanceState.getInt("score", 0)
// UI 업데이트
updateUI()
}
}
③ savedStateHandle 사용하기 (권장)
최신 안드로이드에서는 ViewModel과 savedStateHandle을 함께 사용하는 방식을 권장합니다.
class MainViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
var userName: String
get() = savedStateHandle.get<String>("user_name") ?: ""
set(value) { savedStateHandle.set("user_name", value) }
}
3. IllegalStateException: 이미 종료된 Activity
Activity가 이미 종료되었거나 종료 중인데 UI를 업데이트하려고 할 때 발생하는 오류입니다.
주요 원인:
- 비동기 콜백에서 Activity가 종료된 후 UI 업데이트 시도
- 생명주기를 고려하지 않은 지연된 작업
해결책:
① Activity 상태 확인하기
UI를 업데이트하기 전에 Activity가 유효한 상태인지 확인합니다.
private fun updateUI(data: String) {
if (!isFinishing && !isDestroyed) {
binding.textView.text = data
}
}
② 생명주기 인식 API 사용하기
LiveData나 Flow와 같은 생명주기 인식 API를 사용하면 자동으로 안전하게 처리됩니다.
viewModel.userData.observe(this) { data ->
// LiveData는 자동으로 생명주기를 인식하므로 안전하게 UI 업데이트
binding.textView.text = data
}
③ 작업 취소 구현하기
코루틴의 경우 생명주기에 맞춰 작업을 자동으로 취소할 수 있습니다.
// Activity나 Fragment의 lifecycleScope 사용
val job = lifecycleScope.launch {
val result = fetchData() // 시간이 오래 걸리는 작업
updateUI(result) // 자동으로 생명주기 확인
}
override fun onDestroy() {
super.onDestroy()
// 명시적으로 취소할 필요 없음 - lifecycleScope가 알아서 처리
}
4. ANR(Application Not Responding)
앱이 메인 스레드에서 오래 걸리는 작업을 실행할 때 발생하는 오류입니다. 시스템이 "앱이 응답하지 않습니다" 대화상자를 표시하게 됩니다.
주요 원인:
- 메인 스레드에서 네트워크 요청 수행
- 메인 스레드에서 파일 I/O 작업 수행
- 무거운 계산 작업을 UI 스레드에서 실행
해결책:
① 코루틴 사용하기
코루틴은 비동기 작업을 간결하게 처리할 수 있게 해줍니다.
// IO 디스패처에서 무거운 작업 수행
lifecycleScope.launch(Dispatchers.IO) {
// 네트워크 요청, 파일 읽기 등 무거운 작업
val data = repository.fetchData()
// UI 업데이트는 메인 스레드에서
withContext(Dispatchers.Main) {
binding.textView.text = data
}
}
② WorkManager 활용하기
백그라운드에서 실행해야 하는 작업, 특히 앱이 종료된 후에도 실행되어야 하는 작업에는 WorkManager를 사용합니다.
val workRequest = OneTimeWorkRequestBuilder<DataSyncWorker>()
.setConstraints(Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build())
.build()
WorkManager.getInstance(context).enqueue(workRequest)
③ 무거운 작업 분리하기
초기화 과정에서 무거운 작업을 분리하고 지연 초기화를 활용합니다.
// 필요할 때만 초기화되는 무거운 객체
private val expensiveResource by lazy {
// 시간이 오래 걸리는 초기화 작업
HeavyResourceManager()
}
private fun onButtonClick() {
// 버튼 클릭 시에만 초기화됨
val result = expensiveResource.process()
}
5. 프래그먼트 트랜잭션 오류
Activity의 상태가 저장된 후 프래그먼트 트랜잭션을 수행할 때 발생하는 오류입니다.
주요 원인:
onSaveInstanceState()
호출 후 프래그먼트 트랜잭션 시도- Activity 종료 중에 새 프래그먼트 추가 시도
해결책:
① 올바른 생명주기 단계에서 트랜잭션 수행하기
프래그먼트 트랜잭션은 Activity가 활성 상태일 때만 수행해야 합니다.
override fun onResumeFragments() {
super.onResumeFragments()
// 이 시점에서 프래그먼트 트랜잭션 수행이 안전함
if (shouldShowDetailsFragment) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, DetailsFragment())
.commit()
}
}
② 상태 확인 후 트랜잭션 수행하기
트랜잭션 전에 Activity 상태를 확인합니다.
private fun showFragment() {
if (!isFinishing && !supportFragmentManager.isStateSaved) {
supportFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment())
.commit()
}
}
③ commitAllowingStateLoss() 사용하기 (주의해서 사용)
정말 필요한 경우에만 commitAllowingStateLoss()
를 사용할 수 있습니다. 하지만 상태 손실 가능성을 염두에 두어야 합니다.
// 상태 손실을 허용하지만, 가능하면 피하는 것이 좋음
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment)
.commitAllowingStateLoss()
6. Activity 스택 관리 문제
Activity 스택을 잘못 관리하면 사용자 경험이 저하되고, 백 버튼 동작이 이상해질 수 있습니다.
주요 원인:
- Intent 플래그를 잘못 사용
- Activity 인스턴스가 중복 생성
- 잘못된 실행 모드(launchMode) 설정
해결책:
① 적절한 launchMode 설정하기
Activity의 실행 모드를 목적에 맞게 설정합니다.
<!-- AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:launchMode="singleTop" />
주요 launchMode 옵션:
standard
: 기본값, 항상 새 인스턴스 생성singleTop
: 최상위에 있을 때만 재사용singleTask
: 태스크당 하나의 인스턴스만 허용singleInstance
: 전용 태스크에서 단일 인스턴스
② Intent 플래그 활용하기
상황에 맞는 Intent 플래그를 사용합니다.
// 기존 인스턴스로 이동하고 그 위의 Activity 모두 제거
val intent = Intent(this, HomeActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(intent)
③ 백 스택 관리하기
TaskStackBuilder를 사용하여 올바른 백 스택을 구성합니다.
// 알림에서 딥링크 처리 시 백 스택 구성
val stackBuilder = TaskStackBuilder.create(this)
stackBuilder.addParentStack(DetailActivity::class.java)
stackBuilder.addNextIntent(detailIntent)
stackBuilder.startActivities()
7. Context 사용 오류
잘못된 Context를 사용하면 예상치 못한 문제가 발생할 수 있습니다.
주요 원인:
- 잘못된 종류의 Context 사용
- Context 누수
- 생명주기가 짧은 Context를 오래 유지
해결책:
① 적절한 Context 유형 사용하기
작업에 맞는 Context 유형을 선택합니다.
// UI 관련 작업 (대화상자, 토스트 등)
val uiContext = this // Activity Context
// 앱 전체 리소스 접근 (데이터베이스, 파일 등)
val appContext = applicationContext
② Context 참조 제한하기
싱글톤이나 정적 변수에 Activity Context를 저장하지 않습니다.
// 잘못된 방법 ❌
object MySingleton {
private var activityContext: Context? = null
fun init(context: Context) {
this.activityContext = context // 메모리 누수 위험
}
}
// 올바른 방법 ✅
object MySingleton {
private var appContext: Context? = null
fun init(context: Context) {
// Application Context만 저장
this.appContext = context.applicationContext
}
}
③ 컨텍스트 참조 정리하기
필요 없어진 Context 참조는 명시적으로 정리합니다.
class MyManager(context: Context) {
private var contextRef = WeakReference(context)
fun doSomething() {
val context = contextRef.get() ?: return
// 작업 수행
}
fun release() {
contextRef.clear() // 참조 정리
}
}
#11. Activity 성능 최적화 팁
1. 레이아웃 최적화
복잡한 레이아웃은 렌더링 성능을 저하시키고 메모리를 더 많이 사용합니다.
① 뷰 계층 간소화하기
중첩된 레이아웃을 최소화하고 ConstraintLayout을 활용합니다.
<!-- 비효율적인 중첩 레이아웃 ❌ -->
<LinearLayout>
<LinearLayout>
<RelativeLayout>
<TextView />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<!-- 효율적인 ConstraintLayout 사용 ✅ -->
<androidx.constraintlayout.widget.ConstraintLayout>
<TextView
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
② ViewBinding 사용하기
ViewBinding은 findViewById보다 효율적이고 타입 안전합니다.
// build.gradle
android {
buildFeatures {
viewBinding true
}
}
// Activity에서 사용
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// findViewById 대신 바인딩 사용
binding.buttonSubmit.setOnClickListener {
// 클릭 처리
}
}
}
③ 불필요한 inflate 줄이기
반복되는 항목은 RecyclerView를 사용하고, ViewHolder 패턴을 적용합니다.
class MyAdapter(private val items: List<Item>) :
RecyclerView.Adapter<MyAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
// inflate는 필요할 때만 수행됨
val binding = ItemLayoutBinding.inflate(
LayoutInflater.from(parent.context), parent, false)
return ViewHolder(binding)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(items[position])
}
class ViewHolder(private val binding: ItemLayoutBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Item) {
binding.textTitle.text = item.title
// 바인딩 작업
}
}
}
2. 생명주기에 맞춘 리소스 관리
리소스를 생명주기에 맞게 관리하면 메모리 누수를 방지하고 앱 성능을 향상시킬 수 있습니다.
① 리소스 초기화 및 해제 타이밍
각 리소스를 적절한 생명주기 메서드에서 초기화하고 해제합니다.
class CameraActivity : AppCompatActivity() {
private var camera: Camera? = null
override fun onResume() {
super.onResume()
// 카메라 초기화
camera = Camera.open()
}
override fun onPause() {
super.onPause()
// 카메라 해제
camera?.release()
camera = null
}
}
② DefaultLifecycleObserver 활용하기
생명주기 관찰자를 사용하여 리소스 관리를 모듈화합니다.
class LocationManager(private val activity: AppCompatActivity) : DefaultLifecycleObserver {
init {
activity.lifecycle.addObserver(this)
}
override fun onResume(owner: LifecycleOwner) {
// 위치 업데이트 시작
startLocationUpdates()
}
override fun onPause(owner: LifecycleOwner) {
// 위치 업데이트 중지
stopLocationUpdates()
}
}
// Activity에서 사용
class MainActivity : AppCompatActivity() {
private lateinit var locationManager: LocationManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 생명주기에 맞게 자동으로 관리됨
locationManager = LocationManager(this)
}
}
③ 이미지 리소스 효율적으로 관리하기
비트맵을 적절한 크기로 로드하고 사용하지 않을 때 해제합니다.
private fun decodeSampledBitmap(res: Resources, resId: Int, reqWidth: Int, reqHeight: Int): Bitmap {
// 이미지 크기 확인
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeResource(res, resId, options)
// 샘플 크기 계산
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
// 리사이즈된 비트맵 로드
options.inJustDecodeBounds = false
return BitmapFactory.decodeResource(res, resId, options)
}
private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
3. 비동기 처리 최적화
효율적인 비동기 처리는 UI 응답성을 향상시키고 ANR을 방지합니다.
① 코루틴 효율적으로 사용하기
코루틴 컨텍스트와 스코프를 적절히 사용합니다.
class MainActivity : AppCompatActivity() {
// 네트워크 요청을 위한 코루틴
private fun fetchData() {
// IO 디스패처에서 실행
lifecycleScope.launch(Dispatchers.IO) {
try {
val result = api.getData()
// UI 업데이트는 메인 스레드에서
withContext(Dispatchers.Main) {
updateUI(result)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
showError(e.message)
}
}
}
}
// 무거운 계산 작업을 위한 코루틴
private fun processData(data: List<Item>) {
// Default 디스패처에서 실행 (CPU 집약적 작업)
lifecycleScope.launch(Dispatchers.Default) {
val processed = data.map { processItem(it) }
withContext(Dispatchers.Main) {
showResults(processed)
}
}
}
}
② 작업 취소 처리하기
사용자가 화면을 떠날 때 불필요한 작업을 취소합니다.
class SearchActivity : AppCompatActivity() {
private var searchJob: Job? = null
private fun performSearch(query: String) {
// 이전 검색 작업 취소
searchJob?.cancel()
// 새 검색 작업 시작
searchJob = lifecycleScope.launch {
delay(300) // 디바운싱
val results = repository.search(query)
updateSearchResults(results)
}
}
override fun onDestroy() {
super.onDestroy()
searchJob?.cancel()
}
}
③ Flow 활용하기
데이터 스트림을 효율적으로 처리하기 위해 Flow를 사용합니다.
class UserViewModel : ViewModel() {
private val _searchResults = MutableStateFlow<List<User>>(emptyList())
val searchResults = _searchResults.asStateFlow()
fun searchUsers(query: String) {
viewModelScope.launch {
// 디바운싱, 필터링, 변환 등을 한 번에 처리
userRepository.searchUsers(query)
.debounce(300) // 타이핑 중 연속 요청 방지
.filter { it.isNotEmpty() }
.map { users -> users.sortedBy { it.name } }
.collect { results ->
_searchResults.value = results
}
}
}
}
// Activity에서 사용
lifecycleScope.launch {
viewModel.searchResults
.collectLatest { users ->
adapter.submitList(users)
}
}
4. 렌더링 성능 향상
UI 렌더링 성능을 최적화하면 앱이 더 부드럽게 작동합니다.
① 하드웨어 가속 활성화
하드웨어 가속을 활성화하여 렌더링 성능을 향상시킵니다.
<!-- AndroidManifest.xml -->
<application
android:hardwareAccelerated="true"
... >
<!-- 특정 Activity만 하드웨어 가속 비활성화 (필요한 경우) -->
<activity
android:name=".LegacyActivity"
android:hardwareAccelerated="false" />
</application>
② 오버드로우 줄이기
불필요한 배경이나 중첩된 투명 뷰를 제거합니다.
// 불필요한 배경 제거
view.background = null
// 투명도 최소화
view.alpha = 1.0f
// 중첩된 뷰에 동일한 배경색 설정 방지
containerView.setBackgroundColor(color)
childView.background = null // 자식 뷰 배경 제거
③ 애니메이션 최적화
Property Animation을 사용하고 불필요한 애니메이션을 제거합니다.
// ObjectAnimator 사용
val animator = ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f)
animator.duration = 300
animator.start()
// 여러 애니메이션을 함께 실행
val animSet = AnimatorSet()
animSet.playTogether(
ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 0f, 100f),
ObjectAnimator.ofFloat(view, View.ALPHA, 0f, 1f)
)
animSet.duration = 300
animSet.start()
5. 메모리 사용 최적화
메모리를 효율적으로 사용하면 앱의 안정성과 성능이 향상됩니다.
① 효율적인 컬렉션 사용하기
상황에 맞는 적절한 컬렉션을 선택합니다.
// 정수 키를 사용하는 경우 HashMap 대신 SparseArray 사용
val userMap = SparseArray<User>()
userMap.put(1, User("John"))
val user = userMap.get(1)
// 문자열 키를 사용하는 경우 ArrayMap 고려
val configMap = ArrayMap<String, String>()
configMap["theme"] = "dark"
② 객체 풀 활용하기
자주 생성되고 폐기되는 객체는 풀링을 통해 재사용합니다.
class ObjectPool<T>(
private val maxSize: Int,
private val factory: () -> T
) {
private val pool = ArrayDeque<T>(maxSize)
fun acquire(): T {
return if (pool.isEmpty()) factory() else pool.removeFirst()
}
fun release(obj: T) {
if (pool.size < maxSize) {
pool.addLast(obj)
}
}
}
// 사용 예시
val rectPool = ObjectPool(50) { Rect() }
fun processRects() {
val rect = rectPool.acquire()
try {
// rect 사용
} finally {
rectPool.release(rect)
}
}
③ 메모리 캐시 구현하기
자주 사용되는 리소스는 메모리에 캐싱합니다.
class BitmapCache {
private val maxMemory = Runtime.getRuntime().maxMemory() / 1024
private val cacheSize = (maxMemory / 4).toInt()
private val memoryCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, bitmap: Bitmap): Int {
// 크기를 KB 단위로 반환
return bitmap.byteCount / 1024
}
}
fun addBitmapToCache(key: String, bitmap: Bitmap) {
if (getBitmapFromCache(key) == null) {
memoryCache.put(key, bitmap)
}
}
fun getBitmapFromCache(key: String): Bitmap? {
return memoryCache.get(key)
}
fun clear() {
memoryCache.evictAll()
}
}
6. 앱 시작 시간 단축
앱 시작 시간은 사용자 경험에 큰 영향을 미칩니다.
① 콜드 스타트 최적화하기
시작 화면을 테마로 구현하여 콜드 스타트 지연을 감춥니다.
<!-- res/drawable/splash_background.xml -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/colorBackground" />
<item>
<bitmap
android:gravity="center"
android:src="@drawable/logo" />
</item>
</layer-list>
<!-- res/values/styles.xml -->
<style name="SplashTheme" parent="Theme.AppCompat.NoActionBar">
<item name="android:windowBackground">@drawable/splash_background</item>
</style>
<!-- AndroidManifest.xml -->
<activity
android:name=".SplashActivity"
android:theme="@style/SplashTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
② 초기화 작업 지연시키기
필수적이지 않은 초기화 작업은 백그라운드로 이동하거나 필요할 때까지 지연시킵니다.
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 필수 초기화만 즉시 수행
initializeCriticalComponents()
// 나머지는 지연 초기화
delayedInit()
}
private fun initializeCriticalComponents() {
// 앱 실행에 꼭 필요한 초기화만 수행
}
private fun delayedInit() {
// 메인 스레드의 유휴 시간에 초기화 예약
Looper.myQueue().addIdleHandler {
// 우선순위가 낮은 초기화 수행
initializeNonCriticalComponents()
false // 한 번만 실행
}
// 또는 백그라운드에서 초기화
Thread {
// 백그라운드 초기화 작업
initializeInBackground()
}.start()
}
}
③ 필요할 때만 로드하기
필요한 기능만 먼저 로드하고 나머지는 필요할 때 로드합니다.
// ViewPager에서 필요한 페이지만 미리 로드
viewPager.offscreenPageLimit = 1
// 지연 초기화 사용
private val heavyFeature by lazy {
// 필요할 때만 초기화됨
HeavyFeature()
}
private fun onFeatureButtonClick() {
// 버튼 클릭 시에만 초기화됨
val result = heavyFeature.doWork()
}
7. 효율적인 데이터 로딩
데이터를 효율적으로 로드하면 앱의 반응성이 향상됩니다.
① 페이징 구현하기
대량의 데이터는 한 번에 모두 로드하지 않고 페이징을 사용합니다.
// Paging 3 라이브러리 사용
class UserPagingSource(private val api: UserApi) : PagingSource<Int, User>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
val page = params.key ?: 1
return try {
val response = api.getUsers(page, params.loadSize)
LoadResult.Page(
data = response.users,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.isLastPage) null else page + 1
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
// ViewModel에서 사용
val userFlow = Pager(
config = PagingConfig(
pageSize = 20,
enablePlaceholders = false,
prefetchDistance = 5
),
pagingSourceFactory = { UserPagingSource(api) }
).flow.cachedIn(viewModelScope)
// Activity에서 사용
lifecycleScope.launch {
viewModel.userFlow.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
② 데이터 프리페칭 구현하기
사용자가 필요로 할 가능성이 높은 데이터를 미리 로드합니다.
class DataPreloader(
private val repository: DataRepository,
private val scope: CoroutineScope
) {
fun preloadRelatedItems(currentItemId: String) {
scope.launch(Dispatchers.IO) {
// 연관 항목 미리 로드
val relatedIds = repository.getRelatedItemIds(currentItemId)
// 병렬로 프리페칭
relatedIds.map { itemId ->
async {
repository.preloadItem(itemId)
}
}.awaitAll()
}
}
}
③ 캐싱 전략 구현하기
데이터를 메모리와 디스크에 적절히 캐싱합니다.
class DataRepository(
private val api: ApiService,
private val database: AppDatabase,
private val preferences: SharedPreferences
) {
// 메모리 캐시
private val memoryCache = LruCache<String, Data>(100)
suspend fun getData(id: String, forceRefresh: Boolean = false): Data {
// 1. 메모리 캐시 확인
memoryCache.get(id)?.let {
if (!forceRefresh) return it
}
// 2. 로컬 데이터베이스 확인
if (!forceRefresh) {
val localData = database.dataDao().getById(id)
if (localData != null) {
memoryCache.put(id, localData)
return localData
}
}
// 3. 네트워크에서 로드
val networkData = api.fetchData(id)
// 4. 캐시에 저장
memoryCache.put(id, networkData)
database.dataDao().insert(networkData)
return networkData
}
}
실제 앱 개발에 적용하기
성능 모니터링 도구
앱 성능을 모니터링하고 개선하기 위한 도구들입니다.
① Android Profiler
메모리, CPU, 네트워크 사용량을 실시간으로 모니터링합니다.
② Android Vitals
Google Play Console에서 실제 사용자 환경에서의 앱 성능을 확인합니다.
③ StrictMode
개발 중에 메인 스레드 차단 및 디스크/네트워크 액세스를 감지합니다.
// 개발 빌드에서만 사용
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build()
)
}
결론
안드로이드 Activity의 자주 발생하는 오류를 해결하고 성능을 최적화하는 방법에 대해 알아보았습니다. 메모리 누수, ANR, 화면 회전 시 데이터 손실 등의 문제는 적절한 방법으로 해결할 수 있습니다. 또한, 레이아웃 최적화, 효율적인 리소스 관리, 비동기 처리 개선 등을 통해 앱의 성능을 향상시킬 수 있습니다.
앱 개발 시 이러한 베스트 프랙티스를 따르면 안정적이고 빠른 앱을 만들 수 있으며, 사용자 경험을 크게 향상시킬 수 있습니다. 가장 중요한 것은 생명주기를 제대로 이해하고 관리하는 것이며, 최신 안드로이드 아키텍처 구성요소를 활용하는 것입니다.
여러분의 앱이 더 안정적이고 빠르게 동작하기를 바랍니다!
긴 글 읽어주셔서 감사합니다
끝.
'■Development■ > 《Android》' 카테고리의 다른 글
[Android ] Intent FLAG 완벽 가이드 : Android 개발자를 위한 필수 지식 (0) | 2020.04.08 |
---|---|
[Android] ArrayList 객체를 Intent로 전달하는 방법 (3) | 2020.04.08 |
[Android] Picsasso vs Glide (0) | 2019.10.01 |
[Android] App Architecture 가이드 2 (0) | 2019.09.23 |
[Android] App Architecture 가이드 1 (0) | 2019.09.23 |