본문 바로가기
개발/Android

[Android] 안드로이드 메모리 누수에 관하여

by du.it.ddu 2025. 2. 4.

최근 운영하고 있는 서비스는 Firebase Crashlytics를 사용해서 앱의 로그를 수집하고 있습니다.
99.99%정도는 크래쉬가 발생하지 않지만 일부 크래쉬가 리포트되고 있는데, 그 중 OOM이 꽤 비중을 차지하고 있습니다.

이미지 또는 동영상이 보이거나 백그라운드에서 센서를 활용하는 등의 기능이 있어서인지 OOM이 크래쉬의 주된 원인이 되고 있는데요.
이러한 OOM의 원인은 메모리에 비해 큰 이미지, 동영상과 같은 것을 로드할때도 발생하지만 메모리 누수에 의해 발생하기도 합니다.

 

메모리 누수

메모리 누수는 필요하지 않은 메모리를 계속 차지하고 있는 것을 말합니다.
사용하지 않는 메모리가 계속 공간을 차지하기 때문에 필요한 메모리들이 할당될 공간이 점점 적어지고 결과적으로 메모리가 부족한 상황에 이르게 됩니다.

안드로이드가 동작하는 JVM 환경에서는 더이상 사용되지 않는 객체의 참조가 GC에 의해 제거되지 않아 발생합니다.
가비지 컬렉터(GC)는 메모리에서 제거해야할 객체의 대상의 참조가 없으면 불필요하다고 판단하여 제거하기 때문입니다.

그렇다면 안드로이드에서 이러한 메모리 누수는 어떤 경우에 발생할까요?

 

안드로이드 메모리 누수 케이스

안드로이드에서 메모리 누수의 케이스는 다양합니다. 모든 케이스를 다룰 순 없고, 어떠한 경우가 메모리 누수를 발생시킬 수 있는 상황인지 대표적인 사례로 설명하겠습니다.

첫번째, Activity Context를 Jetpack ViewModel에 전달할 때 발생합니다.

class MyActivity : AppCompatActivity() {
    val viewModel: MyViewModel by viewModels()
    
    private fun doSomething() {
        viewModel.doSomething(this)
    }
}

class MyViewModel : ViewModel() {
    fun doSomething(context: Context) {
        viewModelScope.launch {
            ...
        }
    }
}

Jetpack ViewModel은 Activity보다 더 긴 라이프사이클을 가지고 있습니다.
Activity가 Destroy가 되고 나서야 비로소 ViewModel이 제거가 되기 때문입니다.

따라서 Activity보다 긴 라이프 사이클을 가진 ViewModel에서 Activity Context를 참조하게 되면 Activity가 Destroy되어 메모리에서 정리가 되어야하지만 ViewModel에서 참조를 유지하기 때문에 메모리에서 제거되지 못합니다. 따라서 메모리 누수에 이르게 됩니다.

따라서 ViewModel에서 Activity Context를 참조하지 않도록 해야 합니다.

두번째, Fragment에서 DataBinding/ViewBinding을 Non-Null로 사용하면 발생합니다.

class MyFragment : Fragment() {
    private lateinit var binding: MyFragmentBinding
    
    override fun onCreateView(...): View? {
        binding = ...
        
        return binding.root
    }
}

Fragment는 View에 대한 라이프사이클을 별도로 가지고 있으며, 이에 따라 View는 생성되고 파괴됨을 반복합니다.
이 과정에서 View가 파괴되었음에도 binding 객체를 Non-Null로 관리하여 이전 객체를 그대로 사용하면 메모리에서 정리되지 못하기 때문에 메모리 누수가 발생합니다.

따라서 Binding 객체를 Nullable로 선언하고 View의 생성과 파괴에 따라 적절하게 처리해야 합니다.

 

안드로이드 메모리 프로파일링

앞선 케이스 외에도 다양한 상황에서 메모리 누수는 발생할 수 있습니다. 개발을 하면서 이런 것들을 신경써서 나지 않게 한다면 제일 좋겠지만, 예상치 못하는 부분에서 발생할 수도 있습니다.

그래서 우리는 개발 후에 앱의 성능, 품질을 개선하기 위해 이러한 요소들을 찾아 개선해야 하는 숙명을 가지고 있습니다.
이 요소를 찾는 방법으로 안드로이드 스튜디오에서 프로파일러를 제공해줍니다.
https://developer.android.com/studio/profile/memory-profiler?hl=ko

 

메모리 프로파일러로 앱의 메모리 사용량 검사  |  Android Studio  |  Android Developers

끊김 현상, 멈춤, 심지어 비정상 종료를 일으킬 수 있는 메모리 누수 및 메모리 변동을 식별하는 데 도움이 되는 Android 프로파일러의 메모리 프로파일러 구성요소를 알아보세요.

developer.android.com

안드로이드 스튜디오 Ladybug Feature Drop | 2024.2.2 기준으로, View > Tool Windows > Profiler로 접근하면 다음과 같은 화면을 볼 수 있습니다.

다른 항목들도 앱의 성능 프로파일링에 도움이 되기 때문에 확인해보면 좋겠습니다만, 이 포스팅에서는 Analyze Memory Usage와 Track Memory Comsumption을 확인해보겠습니다.

Analyze Memory Usage

에뮬레이터 또는 기기에서 앱을 실행하고 이 버튼을 누르면 현재 앱의 힙 메모리를 덤프해 줍니다. 그 결과로 다음과 같은 화면을 볼 수 있습니다.

현재 앱에서 사용한 모든 클래스에 대한 메모리 할당과 크기같은 것들을 확인할 수 있습니다. 보시면 노란색 경고 아이콘과 함께 숫자가 나타나 있는걸 볼 수 있는데, 저것이 메모리 누수를 나타냅니다.
Show all classes에서 Show activity/fragment leaks로 변경해보거나 스크롤을 내려봅시다.

어느곳에서 메모리 누수가 발생했는지 확인할 수 있습니다. 제 앱의 LoginActivity에서 메모리 누수가 발생했네요.
ReportFragment는 제 앱의 코드가 아니므로 눈을 가리도록 합시다.

 

Track Memory Comsumption

에뮬레이터 또는 기기에서 앱을 실행하고 이 버튼을 누르면 현재 앱의 메모리 상황을 보여줍니다. 다음과 같은 화면을 볼 수 있습니다.

현재 실행되어있는 액티비티, 그리고 각 영역의 메모리 크기와 같은 데이터를 확인할 수 있습니다.
앱을 사용해보면서 메모리가 비약적으로 높아지거나 회수가 안되는 지점을 확인해볼 수 있겠죠.

하단에 Table 탭을 Visualization 탭으로 변경하면 다음과 같이 좀 더 시각화된 데이터를 볼 수 있는데, 굉장히 복잡해서 분석은 쉽지 않습니다.


메모리 프로파일러를 통해 앱의 메모리 사용량과 누수의 분석에 대해 알아보았습니다.
그런데 이것으로 메모리 누수의 원인을 파악하고 제거하기는 사실 쉽지 않습니다. 좋은 방법이 없을까요?

 

Leakcanary

https://square.github.io/leakcanary/

 

LeakCanary

🤔 Documentation issue? Report or edit LeakCanary 🐤 LeakCanary is a memory leak detection library for Android. LeakCanary’s knowledge of the internals of the Android Framework gives it a unique ability to narrow down the cause of each leak, helping

square.github.io

아주 고맙게도, 메모리 누수의 탐지와 원인을 분석하기 쉽도록 오픈소스가 제공되고 있습니다. 스퀘어를 찬양합시다.
다음과 같이 의존성 설정을 하면 디버그앱 실행 시 자동으로 동작합니다.

dependencies {
  // debugImplementation because LeakCanary should only run in debug builds.
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
}

앱에 메모리 누수가 있다면 Leakcanary가 이를 탐지하고 분석하여 알림을 줍니다. 알림을 통해 위와 같은 화면을 진입할 수 있으며 각 항목에 대한 로그를 세세하게 보여줍니다. 우리는 이 로그를 따라 의심되는 지점을 찾아 메모리 누수를 제거할 수 있습니다.

 

Out Of Memory 핸들링

아무리 메모리 누수를 제거했다 하더라도 사용자의 기기, 앱 사용량, 앱 기능 등에 의해 메모리는 얼마든지 터져나갈 수 있습니다.
사용자는 다수의 앱을 실행하고 있기 때문에 다른 앱에 의해 메모리가 가득차게 되면 프로세스 우선순위에 따라 우리의 앱이 사용할 메모리가 부족해져 종료시켜버리기 때문이죠.

우리는 이런 상황을 어떻게 핸들링할 수 있을까요?

https://developer.android.com/topic/performance/memory?hl=ko

 

앱 메모리 관리  |  App quality  |  Android Developers

Android용으로 개발할 때 사전에 메모리 사용량을 줄이는 방법을 알아봅니다.

developer.android.com

 

안드로이드는 메모리 관리를 위해 onTrimMemory를 통해 메모리의 문제에 대한 알림을 줍니다.

class MyApp : Application() {
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        // do something
    }
}

class MyActivity : AppCompatActivity() {
    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        // do something
    }
}

어플리케이션 클래스 또는 액티비티 등에서 onTrimMemory를 오버라이드 할 수 있고 이에 따라 핸들링할 수 있습니다.
여기서 level은 다음과 같습니다.

  • ComponentCallbacks2.TRIM_MEMORY_BACKGROUND
  • ComponentCallbacks2.TRIM_MEMORY_COMPLETED (SDK 35에서 deprecated)
  • ComponentCallbacks2.TRIM_MEMORY_MODERATE (SDK 35에서 deprecated)
  • ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL (SDK 35에서 deprecated)
  • ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW (SDK 35에서 deprecated)
  • ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE
  • ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN

각각의 레벨에 따라 메모리 위험도가 다릅니다. API 35 이후부터 많은 필드가 deprecated 되었습니다.
이에 더해 다음과 같이 직접 메모리에 대한 정보를 제공받을 수 있습니다.

fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

fun doSomethingMemoryIntensive() {
    val isLowMemory = getAvailableMemory().lowMemory
    // do something
}

이 두 가지를 조합해서 앱의 메모리가 어떠한지 확인하고 적절한 행동을 취할 수 있겠습니다.
예를들어 메모리나 적다면 리소스를 제거한다거나, 사용자에게 알림을 주거나, 앱을 종료시키거나 등이 있겠죠.

 

Thread.setDefaultUncaughtExceptionHandler

우리가 앱에서 예외를 아무리 처리해도 놓치는 경우 또는 OOM의 발생은 앱의 크래쉬로 이어집니다.
이 크래쉬를 막을 순 없지만, 오류를 수집하거나 앱의 재실행 등은 가능할 수 있습니다. 예를들어 다음과 같습니다.

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        Thread.setDefaultUncaughtExceptionHandler { thread, e ->
            if (e is OutOfMemoryError) {
                handleOOM()
            } else {
                handleUnexpectedException(e)
            }
        }
    }

    private fun handleOOM() {
        // do something
    }
    
    private fun handleUnexpectedException(e: Throwable) {
        // do something
    }
}

이렇게 하면 OOM 또는 놓친 예외에 대한 최후의 처리를 할 수 있습니다.

 

앱의 성능과 품질, 안정성을 위해서 메모리 관리는 늘 신경쓰고 작업해야하는 부분입니다. 그러나 개발하면서 신경써도 틈새가 생기기 마련이기 때문에, 이를 적절히 검출하고 처리하는 방법을 알고 있는 것이 좋습니다.

반응형