Android 앱 메모리 누수 해결 방법 — LeakCanary, Fragment, WebView, Bitmap 실전 가이드

Android 앱을 개발하다 보면 처음에는 정상적으로 동작하지만 시간이 지날수록 앱이 느려지는 문제가 발생할 수 있습니다.

특히 여러 화면을 반복해서 이동하거나, 사진을 많이 표시하거나, WebView를 여러 개 사용하는 앱에서 이러한 현상을 경험하기 쉽습니다.

대표적인 증상은 다음과 같습니다.

  • 화면을 이동할수록 메모리 사용량이 증가함

  • Fragment를 닫았는데 메모리가 회수되지 않음

  • WebView를 반복해서 열면 앱이 느려짐

  • 고해상도 이미지를 표시한 뒤 메모리 사용량이 급격히 증가함

  • 앱을 오래 사용하면 OutOfMemoryError가 발생함

  • Activity가 종료되었는데도 이전 Activity 객체가 남아 있음

  • 화면 회전 후 동일한 데이터가 중복으로 로딩됨

이러한 현상의 원인 중 하나가 **메모리 누수(Memory Leak)**입니다.

하지만 메모리 사용량이 증가한다고 해서 모두 메모리 누수인 것은 아닙니다. Android Runtime의 메모리 관리 방식, 이미지 캐시, WebView 프로세스 등으로 인해 정상적인 메모리 사용량도 증가할 수 있습니다.

따라서 메모리 누수를 해결하려면 단순히 메모리를 강제로 정리하는 것이 아니라, 더 이상 필요하지 않은 객체가 왜 계속 참조되고 있는지 확인하는 것이 중요합니다.

이번 글에서는 LeakCanary와 Android Studio Memory Profiler를 이용해 메모리 누수를 분석하고, Kotlin 코드에서 자주 발생하는 문제를 해결하는 방법을 살펴보겠습니다.

작성 기준: 2026년 10월


1. Android 메모리 누수란?

Android 앱은 Kotlin 또는 Java 객체를 힙(Heap) 메모리에 생성합니다.

Android Runtime의 가비지 컬렉터(GC)는 더 이상 접근할 수 없는 객체의 메모리를 회수합니다.

그런데 사용이 끝난 객체가 다른 객체로부터 계속 참조되고 있다면 가비지 컬렉터는 해당 객체를 회수하지 못할 수 있습니다.

예를 들어 다음과 같은 구조입니다.

Application
    │
    ▼
Singleton
    │
    ▼
Activity 참조
    │
    ▼
View 계층
    │
    ├── ImageView
    ├── RecyclerView
    └── WebView

Activity가 종료되었더라도 Singleton이 Activity를 계속 참조하고 있다면 Activity와 관련된 객체가 메모리에 남을 수 있습니다.

이러한 참조 관계가 메모리 누수의 대표적인 원인입니다.

메모리 누수와 메모리 사용량 증가의 차이

구분설명
정상적인 메모리 증가필요한 객체나 캐시가 생성되어 메모리 사용량 증가
메모리 누수사용이 끝난 객체가 불필요한 참조 때문에 회수되지 않음
일시적인 메모리 증가이미지 디코딩이나 대량 데이터 처리 중 사용량 증가
OutOfMemoryError메모리 할당에 실패하여 오류 발생

메모리 누수가 있다고 해서 반드시 즉시 OutOfMemoryError가 발생하는 것은 아닙니다.

반대로 OutOfMemoryError가 발생했다고 해서 반드시 메모리 누수가 존재하는 것도 아닙니다.

2. Android 앱에서 메모리 누수가 발생하는 주요 원인

실무에서 자주 확인하는 원인은 다음과 같습니다.

원인대표적인 상황
Activity Context 보관Singleton에서 Activity 참조 유지
Fragment ViewBindingonDestroyView 이후 Binding 유지
Handler와 Runnable화면 종료 후 예약 작업 실행
Coroutine화면보다 오래 살아 있는 작업에서 View 참조
WebView파괴되지 않은 WebView 또는 콜백 참조
RecyclerViewAdapter가 Fragment View를 계속 참조
Bitmap고해상도 이미지를 대량 보관
Listener등록한 리스너를 해제하지 않음
Observer잘못된 LifecycleOwner로 View 관찰
static 참조오래 유지되는 객체에서 Activity 참조

이 중 Fragment ViewBinding, WebView, Bitmap은 XML 기반 앱에서 특히 자주 점검하게 되는 항목입니다.

3. LeakCanary란?

LeakCanary는 Android 앱에서 메모리 누수를 탐지하고 분석할 수 있도록 도와주는 오픈소스 라이브러리입니다.

주요 기능은 다음과 같습니다.

  • 파괴된 Activity가 메모리에 남아 있는지 감지

  • Fragment 및 Fragment View 누수 탐지

  • 객체의 참조 경로 분석

  • 의심되는 메모리 누수 원인 표시

  • 힙 덤프 분석

LeakCanary는 메모리 누수를 발견하면 어떤 객체가 어떤 참조를 통해 유지되고 있는지 보여줍니다.

단, LeakCanary가 표시하는 모든 retained object가 반드시 실제 버그라는 의미는 아닙니다.

객체의 생명주기와 참조 관계를 함께 분석해야 합니다.

4. LeakCanary 설치하기

app/build.gradle.kts에 디버그 전용 의존성을 추가합니다.

dependencies {
    debugImplementation(
        "com.squareup.leakcanary:leakcanary-android:<version>"
    )
}

<version>은 공식 저장소에서 확인한 안정 버전으로 변경합니다.

LeakCanary는 일반적으로 별도의 초기화 코드 없이 디버그 빌드에서 동작하도록 사용할 수 있습니다.

debugImplementation을 사용하는 이유는 릴리스 앱에 불필요한 디버깅 기능을 포함하지 않기 위해서입니다.

설치 후 앱을 실행하고 Activity나 Fragment를 열었다가 닫으면 LeakCanary가 회수되지 않은 객체를 감지할 수 있습니다.

공식 저장소:

https://github.com/square/leakcanary

5. LeakCanary로 메모리 누수 재현하기

예를 들어 다음과 같은 앱 구조가 있다고 가정하겠습니다.

MainActivity
    │
    └── ViewPager2
           │
           ├── HomeFragment
           ├── SearchFragment
           └── WebFragment

WebFragment에는 WebView가 포함되어 있습니다.

메모리 누수를 확인하려면 다음 순서로 테스트할 수 있습니다.

  1. 디버그 빌드로 앱을 실행합니다.

  2. WebFragment가 포함된 화면으로 이동합니다.

  3. WebView에서 페이지를 로딩합니다.

  4. 해당 화면을 종료하거나 Fragment의 View가 파괴되도록 이동합니다.

  5. LeakCanary의 탐지 결과를 확인합니다.

  6. 누수 의심 객체의 참조 경로를 분석합니다.

주의할 점은 단순히 다른 탭으로 이동했다고 Fragment가 즉시 파괴되는 것은 아니라는 것입니다.

ViewPager2는 인접 페이지를 유지할 수 있으므로 실제로 onDestroyView()가 호출되었는지 확인해야 합니다.

6. Activity Context를 Singleton에 저장하는 문제

가장 대표적인 메모리 누수 사례 중 하나입니다.

다음 코드를 보겠습니다.

object AppManager {

    lateinit var context: Context

    fun initialize(context: Context) {
        this.context = context
    }
}

Activity에서 다음과 같이 호출한다고 가정하겠습니다.

AppManager.initialize(this)

이 경우 Singleton인 AppManager가 Activity Context를 계속 참조할 수 있습니다.

Activity가 종료되더라도 참조가 남아 있으면 메모리가 회수되지 않을 수 있습니다.

해결 방법

Application Context를 사용합니다.

object AppManager {

    private lateinit var appContext: Context

    fun initialize(context: Context) {
        appContext = context.applicationContext
    }
}

Application Context는 앱 프로세스의 생명주기에 맞춰 유지되므로 Activity를 불필요하게 보관하는 문제를 줄일 수 있습니다.

단, Application Context가 모든 Context 사용 상황을 대체할 수 있는 것은 아닙니다.

Activity와 연결된 테마, 창, 사용자 인터페이스가 필요한 작업에는 적절한 Activity Context를 사용해야 합니다.

핵심은 Activity Context를 Activity보다 오래 살아 있는 객체에 보관하지 않는 것입니다.

7. Fragment ViewBinding 메모리 누수

Fragment에서 ViewBinding을 사용하는 경우 View의 생명주기를 반드시 고려해야 합니다.

다음과 같은 코드는 문제가 될 수 있습니다.

class SearchFragment : Fragment() {

    private lateinit var binding: FragmentSearchBinding

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        binding = FragmentSearchBinding.inflate(
            inflater,
            container,
            false
        )

        return binding.root
    }
}

이 구조에서는 Fragment의 View가 파괴된 뒤에도 Fragment 객체가 Binding을 계속 참조할 수 있습니다.

Fragment는 View보다 오래 살아 있을 수 있기 때문입니다.

해결 방법

class SearchFragment : Fragment() {

    private var _binding: FragmentSearchBinding? = null

    private val binding: FragmentSearchBinding
        get() = requireNotNull(_binding)

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        _binding = FragmentSearchBinding.inflate(
            inflater,
            container,
            false
        )

        return binding.root
    }

    override fun onDestroyView() {
        _binding = null
        super.onDestroyView()
    }
}

이렇게 하면 Fragment View가 파괴될 때 Binding 참조를 해제할 수 있습니다.

하지만 Binding을 null로 변경하는 것만으로 모든 누수가 해결되지는 않습니다.

Adapter, Listener, WebView 등에서 View를 참조하고 있는지도 함께 확인해야 합니다.

8. RecyclerView Adapter가 View를 참조하는 문제

Fragment에서 RecyclerView Adapter를 관리할 때도 메모리 누수가 발생할 수 있습니다.

예를 들어 다음과 같은 구조입니다.

private lateinit var adapter: BirdAdapter

Adapter가 RecyclerView 또는 ViewHolder와 관련된 참조를 유지하고 있다면 Fragment의 View가 파괴된 뒤에도 객체가 남을 수 있습니다.

Fragment의 View 생명주기에 맞춰 Adapter 연결을 해제하는 방법을 고려할 수 있습니다.

override fun onDestroyView() {

    binding.recyclerView.adapter = null

    _binding = null

    super.onDestroyView()
}

이 코드는 RecyclerView와 Adapter 사이의 참조를 분리하는 데 도움이 됩니다.

다만 Adapter 자체가 반드시 메모리 누수를 발생시키는 것은 아닙니다.

Adapter가 어떤 객체를 참조하는지, Fragment가 Adapter를 얼마나 오래 보관하는지 확인해야 합니다.

또한 Adapter를 재사용하도록 설계했다면 상태 유지와 데이터 갱신 정책을 함께 고려해야 합니다.

9. Handler와 Runnable 메모리 누수

Android에서는 Handler를 이용해 일정 시간 후 작업을 실행할 수 있습니다.

Handler(Looper.getMainLooper()).postDelayed({
    binding.textView.text = "완료"
}, 10000)

이 코드가 Fragment에서 실행되었다고 가정하겠습니다.

10초가 지나기 전에 Fragment View가 파괴되면 예약된 작업이 이전 View를 참조할 수 있습니다.

해결 방법

Runnable을 명시적으로 관리하고 View가 파괴될 때 취소합니다.

private val handler = Handler(Looper.getMainLooper())

private val updateRunnable = Runnable {
    _binding?.textView?.text = "완료"
}

private fun scheduleUpdate() {
    handler.postDelayed(updateRunnable, 10000)
}

override fun onDestroyView() {
    handler.removeCallbacks(updateRunnable)

    _binding = null

    super.onDestroyView()
}

이렇게 하면 불필요한 예약 작업을 제거할 수 있습니다.

단, Handler를 사용하지 않아도 되는 경우에는 Coroutine을 활용하는 방법도 있습니다.

10. Coroutine으로 예약 작업 관리하기

Fragment의 View와 관련된 작업은 viewLifecycleOwner.lifecycleScope를 사용할 수 있습니다.

viewLifecycleOwner.lifecycleScope.launch {

    delay(10000)

    binding.textView.text = "완료"
}

이 Coroutine은 Fragment View의 생명주기에 연결됩니다.

View가 파괴되면 해당 LifecycleScope의 작업이 취소됩니다.

따라서 Handler의 예약 작업을 직접 제거하는 것보다 관리하기 쉬운 경우가 많습니다.

하지만 다음과 같이 장시간 유지되는 Scope에서 View를 참조하면 문제가 발생할 수 있습니다.

CoroutineScope(SupervisorJob() + Dispatchers.Main).launch {
    delay(10000)
    binding.textView.text = "완료"
}

이 코드는 Scope의 취소 시점이 명확하지 않습니다.

따라서 Activity나 Fragment에서 임의의 CoroutineScope를 생성한다면 반드시 취소 정책을 정해야 합니다.

11. WebView 메모리 누수 문제

WebView는 Android 앱에서 비교적 많은 리소스를 사용하는 컴포넌트입니다.

HTML 렌더링, JavaScript 실행, 네트워크 요청, 이미지 처리 등이 포함되기 때문입니다.

특히 다음과 같은 구조에서는 메모리 관리가 중요합니다.

ViewPager2
    │
    ├── Fragment A
    │     └── WebView A
    │
    ├── Fragment B
    │     └── WebView B
    │
    └── Fragment C
          └── WebView C

여러 WebView가 동시에 생성되면 메모리 사용량이 증가할 수 있습니다.

하지만 이것만으로 메모리 누수라고 판단할 수는 없습니다.

실제 문제가 되는 것은 WebView를 더 이상 사용하지 않는데도 참조가 남아 있거나, 생명주기에 맞춰 리소스를 정리하지 못하는 상황입니다.

12. WebView를 안전하게 정리하기

Fragment에서 WebView를 완전히 폐기하는 경우 다음과 같은 구조를 사용할 수 있습니다.

private var webView: WebView? = null

override fun onDestroyView() {

    webView?.let { current ->

        current.stopLoading()

        (current.parent as? ViewGroup)
            ?.removeView(current)

        current.destroy()
    }

    webView = null

    super.onDestroyView()
}

여기서 중요한 점은 WebView를 실제로 더 이상 사용하지 않는 경우에만 destroy()를 호출해야 한다는 것입니다.

WebView를 재사용하도록 설계했다면 무조건 파괴하면 안 됩니다.

또한 WebView에 등록한 콜백이나 JavaScript 인터페이스가 Activity 또는 Fragment를 참조하는지 확인해야 합니다.

WebView를 정리했다고 해서 해당 객체를 참조하는 모든 외부 리스너가 자동으로 정리되는 것은 아닙니다.

13. addJavascriptInterface와 Activity 참조

WebView에서 JavaScript 인터페이스를 사용하는 경우 다음과 같은 코드를 작성할 수 있습니다.

class JsBridge(
    private val activity: Activity
) {

    @JavascriptInterface
    fun sendMessage(message: String) {
        activity.runOnUiThread {
            Log.d("JsBridge", message)
        }
    }
}

이 구조에서는 JsBridge가 Activity를 직접 참조합니다.

WebView 또는 인터페이스 객체가 Activity보다 오래 유지된다면 메모리 누수로 이어질 수 있습니다.

따라서 JavaScript 인터페이스의 생명주기를 WebView와 함께 관리하고, 필요하지 않은 Activity 참조를 장기간 보관하지 않도록 설계해야 합니다.

예를 들어 ViewModel이나 Repository로 데이터를 전달하는 구조를 사용하면 Activity에 대한 직접 의존성을 줄일 수 있습니다.

단, JavaScript 인터페이스는 보안상 주의가 필요한 기능입니다.

신뢰할 수 없는 웹페이지에 민감한 네이티브 기능을 노출하지 않아야 합니다.

14. Bitmap으로 인한 메모리 사용량 증가

고해상도 이미지를 직접 디코딩하면 많은 메모리를 사용할 수 있습니다.

예를 들어 4000×3000 크기의 이미지를 ARGB_8888 형식으로 디코딩한다고 가정하겠습니다.

픽셀당 약 4바이트를 사용하므로 단순 계산으로 다음과 같습니다.

4000 × 3000 × 4
= 48,000,000 bytes
≈ 45.8 MiB

이미지 한 장만으로도 상당한 메모리를 사용할 수 있습니다.

물론 실제 메모리 사용량은 Bitmap 설정, 이미지 형식, 디코딩 방식 등에 따라 달라질 수 있습니다.

문제는 여러 이미지를 동시에 메모리에 보관하는 경우입니다.

특히 RecyclerView에서 이미지 수십 장을 직접 디코딩하면 메모리 사용량이 급격히 증가할 수 있습니다.

15. BitmapFactory로 이미지 크기 줄이기

이미지를 직접 디코딩해야 한다면 inSampleSize를 이용해 크기를 줄일 수 있습니다.

fun decodeSampledBitmap(
    path: String,
    reqWidth: Int,
    reqHeight: Int
): Bitmap? {

    val options = BitmapFactory.Options().apply {
        inJustDecodeBounds = true
    }

    BitmapFactory.decodeFile(path, options)

    options.inSampleSize = calculateInSampleSize(
        options,
        reqWidth,
        reqHeight
    )

    options.inJustDecodeBounds = false

    return BitmapFactory.decodeFile(path, options)
}

inSampleSize를 계산하는 함수는 다음과 같이 구현할 수 있습니다.

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
}

이 코드는 이미지 크기를 줄여 디코딩하는 기본적인 예시입니다.

실제 프로젝트에서는 요청 크기가 0 이하인 경우나 이미지 파일이 손상된 경우도 처리해야 합니다.

또한 이미지 디코딩은 메인 스레드에서 직접 실행하지 않는 것이 좋습니다.

16. Glide를 이용한 이미지 메모리 관리

RecyclerView나 상세 화면에서 이미지를 표시한다면 Glide 같은 이미지 로딩 라이브러리를 사용하는 것이 편리합니다.

Glide.with(imageView)
    .load(imageUrl)
    .placeholder(R.drawable.ic_placeholder)
    .error(R.drawable.ic_placeholder)
    .into(imageView)

Glide는 이미지 로딩과 디코딩, 캐싱, View 재활용에 필요한 기능을 제공합니다.

여기서 중요한 점은 다음 코드입니다.

Glide.with(imageView)

Glide 요청을 해당 View의 생명주기와 연관 지어 관리하는 데 도움이 됩니다.

Fragment에서 사용하는 경우에도 적절한 Context나 View를 전달해 요청의 생명주기를 관리해야 합니다.

또한 Glide 캐시가 메모리를 사용하고 있다는 이유만으로 메모리 누수라고 판단해서는 안 됩니다.

캐시는 성능 향상을 위해 의도적으로 유지되는 데이터입니다.

17. RecyclerView 이미지 요청 정리하기

RecyclerView에서는 ViewHolder가 재활용됩니다.

따라서 이전 이미지 요청이 남아 있으면 잘못된 이미지가 표시되거나 불필요한 리소스를 사용할 수 있습니다.

Glide는 일반적인 View 재활용 상황을 처리하도록 설계되어 있지만, 복잡한 커스텀 로딩 로직에서는 요청 정리가 필요할 수 있습니다.

override fun onViewRecycled(holder: BirdViewHolder) {
    Glide.with(holder.imageView)
        .clear(holder.imageView)

    super.onViewRecycled(holder)
}

이 코드는 ViewHolder가 재활용될 때 해당 ImageView의 Glide 요청을 정리하는 예시입니다.

모든 RecyclerView에서 반드시 수동으로 clear()를 호출해야 하는 것은 아닙니다.

기본적인 Glide 사용만으로도 적절하게 관리되는 경우가 많으므로 실제 문제가 있는지 확인한 뒤 적용하는 것이 좋습니다.

18. LiveData Observer와 생명주기

Fragment에서 LiveData를 관찰할 때는 적절한 LifecycleOwner를 사용하는 것이 중요합니다.

다음과 같은 코드를 생각해 보겠습니다.

viewModel.birds.observe(this) { birds ->
    binding.recyclerView.adapter?.notifyDataSetChanged()
}

Fragment에서 this를 사용하면 Fragment의 생명주기에 Observer가 연결됩니다.

하지만 Fragment의 View는 Fragment보다 먼저 파괴될 수 있습니다.

따라서 View를 갱신하는 Observer는 다음과 같이 작성하는 것이 좋습니다.

viewModel.birds.observe(viewLifecycleOwner) { birds ->
    adapter.submitList(birds)
}

이렇게 하면 Fragment View의 생명주기에 맞춰 Observer를 관리할 수 있습니다.

특히 ViewPager2에서 Fragment의 View가 생성되고 파괴되는 상황에 적합합니다.

19. Android Studio Memory Profiler 사용하기

LeakCanary 외에도 Android Studio의 Memory Profiler를 사용할 수 있습니다.

Memory Profiler에서는 다음과 같은 정보를 확인할 수 있습니다.

  • 앱의 메모리 사용량 변화

  • Java/Kotlin 힙 사용량

  • 객체 할당 현황

  • 힙 덤프

  • 특정 객체의 참조 관계

예를 들어 WebView가 포함된 Fragment를 반복해서 열고 닫는 상황을 테스트할 수 있습니다.

앱 실행
   │
   ▼
WebFragment 진입
   │
   ▼
WebView 페이지 로딩
   │
   ▼
WebFragment 종료
   │
   ▼
메모리 사용량 확인
   │
   ▼
반복 테스트

반복 테스트 후 Activity나 Fragment 인스턴스가 계속 증가하는지 확인합니다.

다만 GC 실행 시점과 캐시 정책에 따라 메모리 사용량은 달라질 수 있습니다.

따라서 단순한 메모리 그래프만으로 누수 여부를 확정하지 말고 힙 덤프와 참조 경로를 함께 분석해야 합니다.

20. WeakReference를 사용하면 해결될까?

메모리 누수 문제를 해결하기 위해 WeakReference를 사용하는 경우가 있습니다.

val weakActivity = WeakReference(activity)

WeakReference는 객체를 강하게 참조하지 않으므로 가비지 컬렉션을 방해하지 않습니다.

하지만 모든 Activity 참조를 WeakReference로 변경하는 것이 올바른 해결 방법은 아닙니다.

예를 들어 Activity가 필요한 시점에 이미 회수되었다면 참조 결과가 null이 될 수 있습니다.

또한 메모리 누수의 근본 원인이 잘못된 생명주기 관리라면 WeakReference로 문제를 숨기기보다 참조 관계를 수정하는 것이 좋습니다.

대부분의 경우 다음과 같은 해결책이 우선입니다.

  • 올바른 LifecycleOwner 사용

  • Listener 등록 및 해제

  • ViewBinding 정리

  • Coroutine 취소

  • WebView 생명주기 관리

  • 불필요한 Activity Context 참조 제거

21. 메모리 누수 테스트 시나리오

실제 프로젝트에서는 다음 상황을 테스트하는 것이 좋습니다.

테스트확인 내용
Activity 반복 진입종료된 Activity가 남는지
Fragment 반복 이동파괴된 View가 유지되는지
ViewPager2 탭 이동불필요한 Fragment·WebView 참조
화면 회전이전 Activity 인스턴스 회수
RecyclerView 스크롤이미지 메모리 사용량
대용량 이미지 표시Bitmap 메모리 증가
WebView 페이지 이동사용하지 않는 WebView 정리
앱 백그라운드 복귀객체 중복 생성 여부
네트워크 요청 중 화면 종료콜백과 Context 참조
광고 화면 이동AdView 생명주기 정리

특히 ViewPager2와 WebView를 함께 사용하는 앱에서는 탭을 빠르게 이동하면서 메모리 사용량과 객체 참조 상태를 확인하는 것이 좋습니다.

22. Android 메모리 누수 예방 체크리스트

  • Singleton에서 Activity Context를 장기간 보관하지 않음

  • Fragment ViewBinding을 onDestroyView()에서 해제

  • RecyclerView Adapter의 View 참조 확인

  • Handler 예약 작업 취소

  • UI Coroutine은 View 생명주기에 연결

  • WebView 폐기 시 destroy() 호출

  • JavaScript 인터페이스의 Activity 참조 확인

  • 고해상도 Bitmap 직접 디코딩 최소화

  • Glide 등 이미지 로딩 라이브러리 활용

  • LiveData Observer에 viewLifecycleOwner 사용

  • Listener와 Callback 등록·해제 관리

  • LeakCanary로 retained object 분석

  • Memory Profiler로 힙 덤프 확인

  • 화면 회전과 Fragment 재생성 테스트

  • 반복 화면 이동 후 객체 회수 여부 검증

23. 마무리

Android 앱의 메모리 누수는 단순히 메모리 사용량이 많다는 문제가 아닙니다.

사용이 끝난 객체가 불필요한 참조 때문에 계속 유지되는 것이 핵심입니다.

특히 Kotlin + XML 기반 앱에서는 다음 항목을 우선적으로 확인하는 것이 좋습니다.

  1. Activity Context를 Singleton에 저장하지 않는지 확인합니다.

  2. Fragment의 ViewBinding을 View 생명주기에 맞춰 정리합니다.

  3. WebView를 생성하고 폐기하는 정책을 명확하게 관리합니다.

  4. Bitmap과 이미지 캐시의 메모리 사용량을 구분합니다.

  5. Coroutine, Handler, Observer가 파괴된 View를 참조하지 않는지 확인합니다.

  6. LeakCanary와 Memory Profiler를 이용해 실제 참조 경로를 분석합니다.

메모리 누수 해결에서 가장 중요한 것은 메모리를 강제로 비우는 것이 아닙니다.

객체의 생명주기와 참조 관계를 정확하게 파악하고, 필요하지 않은 참조를 적절한 시점에 해제하는 것이 중요합니다.

이러한 원칙을 적용하면 앱을 장시간 사용하거나 여러 화면을 반복해서 이동하는 상황에서도 안정적인 동작을 기대할 수 있습니다.


참고 자료

태그: Android, Kotlin, Memory Leak, LeakCanary, Memory Profiler, Fragment, ViewBinding, WebView, RecyclerView, Glide, Bitmap, Coroutine, MVVM, 메모리 최적화