Android

Android 앱 성능 최적화 전략 - 메모리 관리부터 ANR 문제 해결까지

임베디드 친구 2024. 11. 28. 08:46
반응형

안드로이드 애플리케이션을 개발할 때, 성능은 사용자 경험에 큰 영향을 미칩니다. 느린 화면 전환, 자주 발생하는 ANR (Application Not Responding) 문제 등은 사용자로 하여금 앱을 떠나게 만들 수 있습니다. 이번 포스팅에서는 안드로이드 애플리케이션의 성능을 최적화하는 다양한 전략들을 소개하고, 각 전략의 실제 예제 코드와 함께 설명하겠습니다. 목표는 앱의 메모리 관리, 스레드 처리, ANR 문제 해결 등 성능 관련 문제들을 어떻게 최적화할 수 있는지 이해하는 것입니다.

1. 메모리 관리 최적화

1.1 가비지 컬렉션(GC) 최소화

안드로이드에서는 자바와 코틀린의 가비지 컬렉터(Garbage Collector, GC)가 메모리 관리를 담당합니다. 하지만 불필요하게 자주 발생하는 GC는 앱의 성능을 떨어뜨릴 수 있습니다. 특히, 불필요한 객체 생성을 최소화하고, 재사용 가능한 객체는 최대한 재사용하는 것이 중요합니다.

예제: RecyclerView에서 ViewHolder 재사용

class MyAdapter(private val itemList: List<String>) : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        val textView: TextView = itemView.findViewById(R.id.textView)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_layout, parent, false)
        return MyViewHolder(view)
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.textView.text = itemList[position]
    }

    override fun getItemCount() = itemList.size
}

RecyclerView는 ViewHolder를 재사용하여 불필요한 객체 생성을 줄입니다. 이를 통해 메모리 사용량을 절감하고 앱의 성능을 최적화할 수 있습니다.

1.2 메모리 릭(Memory Leak) 방지

메모리 릭은 앱의 메모리를 계속 점유하게 만들어 결국 OutOfMemoryError를 발생시킬 수 있습니다. 액티비티의 참조를 유지한 채로 액티비티가 소멸되지 않도록 주의해야 합니다.

예제: Context 사용 주의

class MySingleton private constructor(context: Context) {
    private val appContext = context.applicationContext

    companion object {
        @Volatile
        private var INSTANCE: MySingleton? = null

        fun getInstance(context: Context): MySingleton {
            return INSTANCE ?: synchronized(this) {
                INSTANCE ?: MySingleton(context).also { INSTANCE = it }
            }
        }
    }
}

싱글톤 패턴을 사용할 때, 액티비티의 Context를 직접 참조하면 메모리 릭이 발생할 수 있습니다. 대신 applicationContext를 사용하여 액티비티의 수명 주기와 관계없이 안전하게 사용할 수 있습니다.

2. ANR 문제 해결

2.1 UI 스레드 차단 방지

ANR은 UI 스레드가 오랫동안 차단될 때 발생합니다. 네트워크 요청이나 디스크 I/O 같은 시간이 오래 걸리는 작업은 UI 스레드에서 수행하지 않도록 해야 합니다.

예제: 비동기 네트워크 요청

class NetworkActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_network)

        val button: Button = findViewById(R.id.button)
        button.setOnClickListener {
            makeNetworkRequest()
        }
    }

    private fun makeNetworkRequest() {
        CoroutineScope(Dispatchers.IO).launch {
            // 네트워크 요청 수행
            val result = URL("https://example.com").readText()
            withContext(Dispatchers.Main) {
                // UI 업데이트
                findViewById<TextView>(R.id.textView).text = result
            }
        }
    }
}

코루틴을 사용하여 네트워크 요청을 Dispatchers.IO에서 수행하고, 결과를 Dispatchers.Main에서 UI에 반영합니다. 이를 통해 UI 스레드가 차단되지 않도록 하여 ANR을 방지할 수 있습니다.

3. 렌더링 성능 최적화

3.1 중복 작업 최소화

UI 렌더링을 최적화하기 위해서는 중복된 작업을 최소화해야 합니다. 예를 들어, onDraw() 메서드에서 불필요한 연산을 수행하는 것은 성능 저하의 원인이 될 수 있습니다.

예제: Custom View의 onDraw 최적화

class OptimizedCustomView(context: Context, attrs: AttributeSet) : View(context, attrs) {
    private val paint = Paint().apply {
        color = Color.BLUE
        style = Paint.Style.FILL
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        canvas.drawCircle(width / 2f, height / 2f, 100f, paint)
    }
}

onDraw() 메서드에서는 Paint 객체를 반복적으로 생성하지 않고, 미리 생성해둔 객체를 재사용하여 성능을 최적화합니다.

4. 배터리 사용량 최적화

4.1 불필요한 백그라운드 작업 줄이기

백그라운드에서 실행되는 작업은 배터리 소모를 증가시킵니다. 작업이 꼭 필요하지 않다면 백그라운드 작업을 줄이는 것이 좋습니다.

예제: WorkManager를 사용한 효율적인 백그라운드 작업

class BatteryOptimizedActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_battery_optimized)

        val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
            .setConstraints(
                Constraints.Builder()
                    .setRequiresCharging(true) // 충전 중일 때만 작업 수행
                    .build()
            )
            .build()

        WorkManager.getInstance(this).enqueue(workRequest)
    }
}

class MyWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        // 백그라운드 작업 수행
        return Result.success()
    }
}

WorkManager를 사용하여 충전 중일 때만 백그라운드 작업을 수행하도록 설정하여 배터리 사용을 최적화할 수 있습니다.

5. 데이터베이스 접근 최적화

5.1 메인 스레드에서 데이터베이스 접근 피하기

Room 데이터베이스 같은 경우에도 메인 스레드에서 접근하면 ANR이 발생할 수 있습니다. 따라서, 비동기로 데이터베이스 작업을 수행해야 합니다.

예제: Room 데이터베이스 비동기 접근

@Dao
interface UserDao {
    @Insert
    suspend fun insertUser(user: User)

    @Query("SELECT * FROM user")
    suspend fun getAllUsers(): List<User>
}

class DatabaseActivity : AppCompatActivity() {

    private lateinit var userDao: UserDao

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_database)

        val db = Room.databaseBuilder(
            applicationContext,
            AppDatabase::class.java, "user-database"
        ).build()

        userDao = db.userDao()

        CoroutineScope(Dispatchers.IO).launch {
            val users = userDao.getAllUsers()
            withContext(Dispatchers.Main) {
                // UI 업데이트
                findViewById<TextView>(R.id.textView).text = users.joinToString { it.name }
            }
        }
    }
}

Room 데이터베이스 접근 시 코루틴을 사용하여 비동기적으로 데이터를 가져오고, 메인 스레드에서 UI를 업데이트하여 ANR 문제를 방지합니다.

결론

안드로이드 앱의 성능 최적화는 사용자 경험을 향상시키기 위한 필수적인 작업입니다. 메모리 관리, ANR 방지, 렌더링 성능, 배터리 사용량, 데이터베이스 접근 최적화 등 다양한 측면에서 성능을 고려해야 합니다. 이번 포스팅에서 소개한 전략과 예제 코드들을 참고하여, 여러분의 앱이 더 빠르고 안정적으로 동작할 수 있도록 최적화해 보세요.

더 나아가 성능을 분석하고 최적화할 수 있는 다양한 도구들(예: Android Profiler)을 활용하여 지속적으로 성능을 개선하는 것도 좋은 방법입니다. 안드로이드 앱 개발은 작은 최적화들이 모여 큰 차이를 만들어내는 과정임을 잊지 마세요.

반응형